|
4551
|
165
|
23
|
2026-05-07T14:08:24.928238+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162904928_m1.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug","depth":4,"on_screen":true,"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.16458334,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.16458334,"top":0.05888889,"width":0.16458334,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.16875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.32916668,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33333334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49340278,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49756944,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6576389,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66180557,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.821875,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.82604164,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.95763886,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.47013888,"top":0.033333335,"width":0.0625,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-4301186685460921314
|
4433835180828689156
|
visual_change
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
4549
|
NULL
|
NULL
|
NULL
|
|
4552
|
165
|
24
|
2026-05-07T14:08:39.423353+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162919423_m1.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS]
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny#","depth":4,"on_screen":true,"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.16458334,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.16458334,"top":0.05888889,"width":0.16458334,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.16875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.32916668,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33333334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49340278,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49756944,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6576389,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66180557,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.821875,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.82604164,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.95763886,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.47013888,"top":0.033333335,"width":0.0625,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
4236593589833150367
|
4433905549572883204
|
visual_change
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS]
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4553
|
165
|
25
|
2026-05-07T14:08:45.417849+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162925417_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelplallLukas/Stefka 121 - in 22 mDOCKERDEV (docker)H82Matchingcontact4Matchingcontact5Matching contact6Matching contact7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contactMatching contact1Matchingcontact2Matching contact3Matchingcontact4MatchingcontactMatchingcontact6Matchingcontact 7Matchingcontact8MoMiAPP (-zsh)DEV (docker)*3-zsh• 84|screenpipe*•₴5100% <78Thu 7 May 17:08:45181-zsh₴6DEV$IPS202203204205→ 206207208209210retry_atter''reason'=>D);PhpStormsretryAfter,Se->getMessage),throw new RateLimitException('Hubspot returned 429', SretryAfter, $e);} else 1throw Se;}+14 vendor frames15app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166SevenShores\Hubspot\Http\Client:: request("POST", "[URL_WITH_CREDENTIALS] ]...
|
NULL
|
1957482492588107614
|
NULL
|
visual_change
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelplallLukas/Stefka 121 - in 22 mDOCKERDEV (docker)H82Matchingcontact4Matchingcontact5Matching contact6Matching contact7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contactMatching contact1Matchingcontact2Matching contact3Matchingcontact4MatchingcontactMatchingcontact6Matchingcontact 7Matchingcontact8MoMiAPP (-zsh)DEV (docker)*3-zsh• 84|screenpipe*•₴5100% <78Thu 7 May 17:08:45181-zsh₴6DEV$IPS202203204205→ 206207208209210retry_atter''reason'=>D);PhpStormsretryAfter,Se->getMessage),throw new RateLimitException('Hubspot returned 429', SretryAfter, $e);} else 1throw Se;}+14 vendor frames15app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166SevenShores\Hubspot\Http\Client:: request("POST", "[URL_WITH_CREDENTIALS] ]...
|
4552
|
NULL
|
NULL
|
NULL
|
|
4554
|
166
|
23
|
2026-05-07T14:08:45.977266+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162925977_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5222591262487654398
|
-8636637129462003327
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
PhostormVIewINavigareCodeLaravelKeractorWindowFV faVsco.js°9 master kProiectT DeleteCrmEntityTrait.phpRateLimitException.php@.gitignoree audio.wav© PaginationState.phpT IntegrationApp/.../SyncCrmEntitiesTrait.phpbasicapl.onp© SyncOpportunitiesJob.php= custom.log= nuospot-lournal-poll.log© ImportOpportunityBatch.php© MatchActivityCrmData.php= laravel.log< phpunit.xmlis ttt.isE oauth-private.key© CachedCrmServiceDecorator.phcw Hubspow.syncermenuuestrai.ona=oauth-public.kev= storage= supervisord.didtext-relav.isonv tests• FeatureM Intearation• M Servicesv 7Unit> Actions•M Comnonent197• M ConfiaurationiM ConsoleM ContracteD Domain•DDTO• @ Enumsm Suonte• @ Exceptions• C fixtures• C GuardsC Helpers• C HttpC IntegrationsInteractionsC Jobs?DActivitv> M AiAutomation>D Audidv M AutomatedReportsC CreateResultsTest.oho 226C RequestGenerateAskJi «4*c) RequestGenerateReno( SendRenortEynirinaSol@ SondRenort JobTest oh 237c) SendRenortMail.lobTecC SendRenortNotGenera>IM CalendarM Crm)m DoalDicke) m Mailbay> D StreaminaMToamclass Hubspotpaginationserviceorivate runction validatelloken rveeded culent Sculent. Paonationstate sstatel: vold....?private function executeSearchRequest(Client Sclient, string Sendpoint, array Spayload, PaginationState $state): arraytry 1Sresponse = $client->getinstance->getClient(->request( method: 'POST', Sendpoint, l'json' => $payLoad]):return $response->toArrayO;} catch (\Exception $e) {i* (Sclient->icllnauthonizedExcention(Sp))4. } plso if (Sclient->icHuhsnotPatelimit(Se)) &SrecryArter = sculent->parserecryArterlse)ySthis->logger->info('[Hubspot) Received 429 from API', [Itoam idi=> $client->getConfig->getTeam->getIdO,"COnT1O10"=> Sclient->getConfiqO->qetId•."recry arter = srecryarter,=> $e->getMessageO.throw new RateLim1tzxcept1on message: "Hubspot returned 429', SretryAtter, Se} else fThrow se:onivate function undatelastRecordId(arnav Soage. PaainationState Sstate)• voidf...}private function getNext0ffset(array $page): inti...}no usadesprivate function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void{...}private function calculateDelayInMicrosecondsO: int{...}SonarQube for INF suadections. Netect more cecurity iccuec in vour DnarQuhe Sorver |l Iearn more / Don't ack adain (todav 10-25)osyncopportunlty.onpProviderkateLimiter.ohoA18 M1 AV$0Lukas sterka 121 • In z2mThu 7 May 17:08:46= custom.log« SF [jiminny@localhost]A HS_local [jiminny@localhost]& console [PROD)& console [STAGING][2026-05-07 14:08:22] local.NOTICE: Monitoring start[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512" "trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}[2026-05-07 14:08:31]Local.LNru. Jaminny console commanas command..run Menory usage derore scaruing command icand". "maLLdox. sk1p-uIsuS.rerresh", "пеmогyberoreсomna[2026-05-07 14:08:31] local.INF0:usage ror commana ""conmand". "marl[2026-05-07 14:08:34] local.INF0: [SocialAccountServicel Fetching token {"socialAccountId":1499 "provider": "hubspot"} {"dorrelacion 10. aszoaata-ssya-4zc1-otos-Useda/t[2026-05-07 14:08:34] local.INF0: [SocialAccountServicel Token retrieved {"socialAccountId":1499, "provider":"hubspot"} {"correlation id":"d326aafa-339a-42c1-bf53-05ebd'EncryptedTokenManager Generating access token.• ""mode": "Leqacy", *"correlation 1d":"d526aata-359a-42C1-bt55-05ebd/t52782", "trace 10"-":148,"team_id":2} {"correlation_id":"d3!IHUbSpor DEBUG certng headens wneadensia watera wnur oylay 2026H08H56 rontent-yoema waool cat on asonHchanseteu %i-810t[Hubspot] Received 429 from API {"team_id":2, "config_id":2,"retry_after":10, "reason": "Client error›awsiatus wa weron Wralmesisade wa wyou have reachher Your secondLy lM AaWA WANON IyDeAWA MRAIENOAW NCONelaiiion CAlA WOngeocL-druncatedecornelat1on101:05260040-3590-4261C065-050h057082TRacе10419029754-[PHONE]-05204618697[stacktrace]#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny(Services(Crm\Hubspot)(PaginationHubspotPaginationService->executeSear#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny||Services|\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(0bject(JiminnThome/GGimiinny/aoo/Servitces/com/Hubsnot7Service.oho/GVIk9P5minny.1Servces.IirmHubsootalalient->detPadinatedbata Anay.contactaD#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny||Services|\Crm\\Hubspot\|Service->matchByName('Robot')ThomeEEimainnv/ann//onsolle//ommands/AGimInnllehudommandbnhnCRa•Giminnv.TonsoleThommandsllwGimiInnvlehud.ommandnattelimite#5/home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\(Jobs\\Jol/home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\Container\\BoundMethod::Illuminate\Container\\{closure}O#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))/home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation#9/home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminatel\Container BoundMethod::call(Object(Illuminatel FoundationApplicatio#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)#11/home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminatel|Console|Command->execute(0bject(SymfonyComponent|Console\Input|\ArgvInput), Object(Il#12/home/jiminny/vendor/Laravel/fnamework/src/Illuminate/Console/Command.php(180): SymfonyComponent||Consolel|Command\|Command->run(Obriect(SymfonyComponentl|Console#13/home/iiminny/vendor/symfony/console/Application.php(1117): Illuminatel|Consolel\Command->run(Obiect(SymfonyComponent||Consolel\Input\\ArgvInput), Obiect(Symfonyl1#14/home/iiminny/vendor/symfony/console/Application.php(356): Symfony|Component||ConsolellApplication->doRunCommand(Obiect(Jiminny|Consolel\Commands|1JiminnyDebugCommEas homeramannyvendor symtony/ consoLeyADolacation. Dho Ci95H syntonyauvonponenta consoLe ADo lucation->dokun ubnect syntony u vomponentaa consoLe uinout lArgvanouon Ubi#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component|\Console\\Application->run(Object(Symfony|\Component\\CoEavhomeraamanny vendor Lanavel anameworkzsrc/auuumanate/ oundation/Adolcaton.oho crosHuom nate avoundation a uonso Le a kennel->handle ubect symtony u vomponent nuo#18 /home/jiminny/artisan(13): Illuminatel\Foundation\Application->handleCommand(Object(Symfony) \Component|\Consolel(Input|\ArgvInput))#19 Smain?IDPeyious excentoNObeCISAVeNShONES WWOSOOt WEXCeoti onsBAdReoues Woooe ự5n O on aA sencen nesn 1e{|"status) "•\"error) " ("message) "•|"You have reached vour secondly Limit.|" ("errorTvpe)"•\"RATE LIMIT\ " |"correlationId\ "-|"019e02c4-d (truncated...)at /home/iiminny/vendor/hubsoot/hubsoot-oho/src/Excentions/HubsootExceotion.ono:241.stacktrace]lFAIOIESE6I1EI1W6eNAoeIS90322119GSe/home/4iminnv/ann/Senvices/Cem/Huhsnot/Pagination/HubsnotPaainationService.nhn(166): SevenShones||Hubsnot||Htto||Client->nequest('P0ST:. 1httns:/lani.huh..., Annav)#2/home/&Eimiinnw//ann//Servilces/Nem/Hih.snot/Padiinat.ion/HuhsnotPadiinatzionSenvilce.nhn/65/FNFimiInnw/lSenvilcestlNem.WIMh.snofalPaqination.luhsnotPadiinatzionSenviices>execuiteSean/home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny|\Services|\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(0bject(Jiminn/home/fiminny/ann/Services/Com/MuhsnotSenvice.nhn@203JiminnvServicesVom.Mubsnot0ient->aetPadinatedDataAnnav./home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')/home/liminny/ann/Console/Commands/AliminnvDehugCommand.nhnCh.iminnvConsoleCommandsWaiminnvDehuafommand->natelimitoWhome/Elimilnnw/Wondoa/AhaavolWEaamewodcec//ihlimiinato//Contaiinor/BoundMothod..nhn/7ADEiminnvllV0oncallallWommandallWEimilnnw0ohua@ommand-Shandiln/ih.iocfT4EimiinnwllWnhallWinbThome/EEminaw/Wondod/AneavellEaamowodWced/Mllmminato/Contained/Mzlnhn/0DhhminatalWontainoaIBoundMothodbatlltm.natallontaiinoalEfailnstnaTa/home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\Containerl(Util::unwrapIfClosure(Obiect(Closure))wa0/home/Eliminaw//vondos//aaave1/Lfaamowoakced/tlminato//Containoa//BoundMothod..nhn/@6= TlluminatelontainoaBoundMothod-0cal1RoundMothod/0hsioct/T1luminataEoundatinw winasun leamsUTF-&io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4555
|
166
|
24
|
2026-05-07T14:08:51.650425+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162931650_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
18
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.9650931,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"value":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.49335107,"top":0.17478053,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.5049867,"top":0.17478053,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n// if ($this->shouldStopPagination($state, $teamId)) {\n// break;\n// }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n// $this->validateTokenIfNeeded($client, $state);\n// usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n// $state->setTotal($page['total'] ?? 0);\n// $this->updateLastRecordId($page, $state);\n//\n// // Safely iterate over results with null check\n// $results = $page['results'] ?? [];\n// foreach ($results as $row) {\n// $state->incrementTotalRecords();\n// yield $row;\n// }\n//\n// $state->setOffset($this->getNextOffset($page));\n// $state->incrementRequestCount();\n//\n// $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n\n yield;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else if ($client->isHubspotRateLimit($e)) {\n $retryAfter = $client->parseRetryAfter($e);\n\n $this->logger->info('[Hubspot] Received 429 from API', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'config_id' => $client->getConfig()->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n// if ($this->shouldStopPagination($state, $teamId)) {\n// break;\n// }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n// $this->validateTokenIfNeeded($client, $state);\n// usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n// $state->setTotal($page['total'] ?? 0);\n// $this->updateLastRecordId($page, $state);\n//\n// // Safely iterate over results with null check\n// $results = $page['results'] ?? [];\n// foreach ($results as $row) {\n// $state->incrementTotalRecords();\n// yield $row;\n// }\n//\n// $state->setOffset($this->getNextOffset($page));\n// $state->incrementRequestCount();\n//\n// $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n\n yield;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else if ($client->isHubspotRateLimit($e)) {\n $retryAfter = $client->parseRetryAfter($e);\n\n $this->logger->info('[Hubspot] Received 429 from API', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'config_id' => $client->getConfig()->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4878437860075686129
|
-6868587801518890065
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
18
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project...
|
4554
|
NULL
|
NULL
|
NULL
|
|
4556
|
165
|
26
|
2026-05-07T14:08:52.261488+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162932261_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5252240045865077800
|
-8204424741936591930
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
iTerm2ShellEditViewSessionScriptsProfilesWindowHelpla6l• Lukas/Stefka 121 • in 22 m100% [8Thu 7 May 17:08:54DOCKERO ₴1DEV (docker)182Matching contact 4Matching contact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0Matching contact 1Matching contact 2Matching contact 3Matching contact 4Matchingcontact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0APP (-zsh)DEV (docker)*3T₴1-zsh• 84|screenpipe*•$5Jiminny \Exceptions \RateLimitExceptionHubspot returned 429at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206202'retry_after' =SretryAfter,203"reason'204= Se->getMessage(),205→ 206throw new RateLimitException('Hubspot returned 429', SretryAfter, $e);207208} else 1throw $e;209}210+14 vendor frames15-zsh₴6DEVapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166SevenShores\Hubspot\Http\Client:: request("POST", "[URL_WITH_CREDENTIALS]...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4557
|
166
|
25
|
2026-05-07T14:08:55.216955+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162935216_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST [URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"168","depth":4,"bounds":{"left":0.96043885,"top":0.07581804,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}","depth":4,"bounds":{"left":0.55019945,"top":0.0726257,"width":0.44980055,"height":0.9273743},"on_screen":true,"value":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"18","depth":4,"bounds":{"left":0.49335107,"top":0.17478053,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.5049867,"top":0.17478053,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n// if ($this->shouldStopPagination($state, $teamId)) {\n// break;\n// }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n// $this->validateTokenIfNeeded($client, $state);\n// usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n// $state->setTotal($page['total'] ?? 0);\n// $this->updateLastRecordId($page, $state);\n//\n// // Safely iterate over results with null check\n// $results = $page['results'] ?? [];\n// foreach ($results as $row) {\n// $state->incrementTotalRecords();\n// yield $row;\n// }\n//\n// $state->setOffset($this->getNextOffset($page));\n// $state->incrementRequestCount();\n//\n// $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n\n yield;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else if ($client->isHubspotRateLimit($e)) {\n $retryAfter = $client->parseRetryAfter($e);\n\n $this->logger->info('[Hubspot] Received 429 from API', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'config_id' => $client->getConfig()->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n// if ($this->shouldStopPagination($state, $teamId)) {\n// break;\n// }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n// $this->validateTokenIfNeeded($client, $state);\n// usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n// $state->setTotal($page['total'] ?? 0);\n// $this->updateLastRecordId($page, $state);\n//\n// // Safely iterate over results with null check\n// $results = $page['results'] ?? [];\n// foreach ($results as $row) {\n// $state->incrementTotalRecords();\n// yield $row;\n// }\n//\n// $state->setOffset($this->getNextOffset($page));\n// $state->incrementRequestCount();\n//\n// $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n\n yield;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else if ($client->isHubspotRateLimit($e)) {\n $retryAfter = $client->parseRetryAfter($e);\n\n $this->logger->info('[Hubspot] Received 429 from API', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'config_id' => $client->getConfig()->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5592488537030429190
|
-4004462544355097179
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST [URL_WITH_CREDENTIALS] HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
// if ($this->shouldStopPagination($state, $teamId)) {
// break;
// }
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
// $this->validateTokenIfNeeded($client, $state);
// usleep($delay);
$page = $this->executeSearchRequest($client, $endpoint, $payload, $state);
// $state->setTotal($page['total'] ?? 0);
// $this->updateLastRecordId($page, $state);
//
// // Safely iterate over results with null check
// $results = $page['results'] ?? [];
// foreach ($results as $row) {
// $state->incrementTotalRecords();
// yield $row;
// }
//
// $state->setOffset($this->getNextOffset($page));
// $state->incrementRequestCount();
//
// $this->logPaginationProgress($state, $teamId, $endpoint);
} while ($state->offset && ! empty($page['results']));
// Log final pagination completion stats
$this->logger->info('[Hubspot] Pagination completed', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'total_requests' => $state->requestCount,
'total_records_fetched' => $state->totalRecords,
'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),
'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,
]);
// Update reference parameters
$total = $state->total;
$lastRecordId = $state->lastRecordId;
yield;
}
private function shouldStopPagination(PaginationState $state, int $teamId): bool
{
if ($state->hasReachedSafetyLimit()) {
$this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [
'team_id' => $teamId,
'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,
'total_fetched' => $state->totalRecords,
]);
return true;
}
return false;
}
private function handlePaginationStrategy(
array $payload,
array $defaultFilter,
PaginationState $state,
int $resultsPerPage,
int $teamId
): array {
if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {
$payload['filters'] = $defaultFilter;
$payload['filters'][] = [
'propertyName' => 'hs_object_id',
'operator' => 'LT',
'value' => $state->lastRecordId,
];
$this->logger->info('[Hubspot] Search keyset pagination request', [
'team_id' => $teamId,
'sequence' => $state->requestCount,
'itemsPerPage' => $resultsPerPage,
'payload' => $payload,
'total' => $state->total,
]);
unset($payload['after']);
$state->setOffset(0);
}
if ($state->offset) {
$payload['after'] = $state->offset;
}
return $payload;
}
private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool
{
// Check if we've hit the offset limit
$shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;
if ($shouldSwitch && $state->lastRecordId === null) {
$this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [
'request_count' => $state->requestCount,
'current_offset' => $state->offset,
'results_per_page' => $resultsPerPage,
'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,
]);
return false; // Continue with offset pagination
}
return $shouldSwitch;
}
private function validateTokenIfNeeded(Client $client, PaginationState $state): void
{
if ($state->shouldValidateToken()) {
$client->ensureValidToken();
$state->updateLastTokenCheck();
}
}
private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array
{
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
} catch (\Exception $e) {
if ($client->isUnauthorizedException($e)) {
$this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'error' => $e->getMessage(),
]);
$client->ensureValidToken();
$state->updateLastTokenCheck();
try {
$response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $response->toArray();
} catch (\Exception $retryException) {
$this->logger->error('[Hubspot] Retry request failed after token refresh', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'original_error' => $e->getMessage(),
'retry_error' => $retryException->getMessage(),
]);
throw $retryException;
}
} else if ($client->isHubspotRateLimit($e)) {
$retryAfter = $client->parseRetryAfter($e);
$this->logger->info('[Hubspot] Received 429 from API', [
'team_id' => $client->getConfig()->getTeam()->getId(),
'config_id' => $client->getConfig()->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
} else {
throw $e;
}
}
}
private function updateLastRecordId(array $page, PaginationState $state): void
{
$lastRecord = ! empty($page['results']) ? end($page['results']) : null;
$lastRecordId = $lastRecord['id'] ?? null;
$state->updateLastRecordId($lastRecordId);
}
private function getNextOffset(array $page): int
{
return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;
}
private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void
{
if ($state->shouldLogProgress()) {
$this->logger->info('[Hubspot] Pagination progress log', [
'team_id' => $teamId,
'endpoint' => $endpoint,
'requests_made' => $state->requestCount,
'records_fetched' => $state->totalRecords,
'elapsed_seconds' => $state->getElapsedSeconds(),
]);
}
}
private function calculateDelayInMicroseconds(): int
{
return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4558
|
165
|
27
|
2026-05-07T14:09:01.929805+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162941929_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {"exception":"[object] (Jiminny\\Exceptions\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)
[stacktrace]
#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
[previous exception] [object] (SevenShores\\Hubspot\\Exceptions\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\Hubspot\\Exceptions\\HubspotException::create(Object(GuzzleHttp\\Exception\\ClientException))
#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#20 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#21 {main}
[previous exception] [object] (GuzzleHttp\\Exception\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)
[stacktrace]
#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\Exception\\RequestException::create(Object(GuzzleHttp\\Psr7\\Request), Object(GuzzleHttp\\Psr7\\Response), NULL, Array, NULL)
#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\Middleware::GuzzleHttp\\{closure}(Object(GuzzleHttp\\Psr7\\Response))
#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\Promise\\Promise::callHandler(1, Object(GuzzleHttp\\Psr7\\Response), NULL)
#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\Promise\\Promise::GuzzleHttp\\Promise\\{closure}()
#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\Promise\\TaskQueue->run(true)
#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\Promise\\Promise->invokeWaitFn()
#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\Promise\\Promise->waitIfPending()
#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\Promise\\Promise->invokeWaitList()
#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\Promise\\Promise->waitIfPending()
#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\Promise\\Promise->wait()
#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\Client->request('POST', 'https://api.hub...', Array)
#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#30 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#31 {main}
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:39] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
Sync Changes
Hide This Notification
Code changed:
Hide
18
1
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"168","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"18","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8364820149347168468
|
-3934659362632824539
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {"exception":"[object] (Jiminny\\Exceptions\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)
[stacktrace]
#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
[previous exception] [object] (SevenShores\\Hubspot\\Exceptions\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\Hubspot\\Exceptions\\HubspotException::create(Object(GuzzleHttp\\Exception\\ClientException))
#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#20 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#21 {main}
[previous exception] [object] (GuzzleHttp\\Exception\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)
[stacktrace]
#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\Exception\\RequestException::create(Object(GuzzleHttp\\Psr7\\Request), Object(GuzzleHttp\\Psr7\\Response), NULL, Array, NULL)
#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\Middleware::GuzzleHttp\\{closure}(Object(GuzzleHttp\\Psr7\\Response))
#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\Promise\\Promise::callHandler(1, Object(GuzzleHttp\\Psr7\\Response), NULL)
#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\Promise\\Promise::GuzzleHttp\\Promise\\{closure}()
#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\Promise\\TaskQueue->run(true)
#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\Promise\\Promise->invokeWaitFn()
#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\Promise\\Promise->waitIfPending()
#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\Promise\\Promise->invokeWaitList()
#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\Promise\\Promise->waitIfPending()
#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\Promise\\Promise->wait()
#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\Client->request('POST', 'https://api.hub...', Array)
#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#30 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#31 {main}
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:39] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
Sync Changes
Hide This Notification
Code changed:
Hide
18
1
Previous Highlighted Error
Next Highlighted Error...
|
4556
|
NULL
|
NULL
|
NULL
|
|
4559
|
166
|
26
|
2026-05-07T14:09:12.739421+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162952739_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST [URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_conta...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"168","depth":4,"bounds":{"left":0.96043885,"top":0.07581804,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}","depth":4,"bounds":{"left":0.11269947,"top":0.0726257,"width":0.88730055,"height":0.9273743},"on_screen":true,"value":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.48238033,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"bounds":{"left":0.49235374,"top":0.17478053,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.5043218,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8319032285102454996
|
5234710935567731044
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST [URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_conta...
|
4557
|
NULL
|
NULL
|
NULL
|
|
4560
|
165
|
28
|
2026-05-07T14:09:21.538488+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162961538_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {"exception":"[object] (Jiminny\\Exceptions\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)
[stacktrace]
#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
[previous exception] [object] (SevenShores\\Hubspot\\Exceptions\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\Hubspot\\Exceptions\\HubspotException::create(Object(GuzzleHttp\\Exception\\ClientException))
#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#20 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#21 {main}
[previous exception] [object] (GuzzleHttp\\Exception\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)
[stacktrace]
#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\Exception\\RequestException::create(Object(GuzzleHttp\\Psr7\\Request), Object(GuzzleHttp\\Psr7\\Response), NULL, Array, NULL)
#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\Middleware::GuzzleHttp\\{closure}(Object(GuzzleHttp\\Psr7\\Response))
#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\Promise\\Promise::callHandler(1, Object(GuzzleHttp\\Psr7\\Response), NULL)
#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\Promise\\Promise::GuzzleHttp\\Promise\\{closure}()
#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\Promise\\TaskQueue->run(true)
#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\Promise\\Promise->invokeWaitFn()
#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\Promise\\Promise->waitIfPending()
#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\Promise\\Promise->invokeWaitList()
#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\Promise\\Promise->waitIfPending()
#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\Promise\\Promise->wait()
#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\Client->request('POST', 'https://api.hub...', Array)
#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#30 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#31 {main}
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:39] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
Sync Changes
Hide This Notification
Code changed:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"168","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5314849083115346115
|
-3934624177186992731
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {"exception":"[object] (Jiminny\\Exceptions\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)
[stacktrace]
#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
[previous exception] [object] (SevenShores\\Hubspot\\Exceptions\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\Hubspot\\Exceptions\\HubspotException::create(Object(GuzzleHttp\\Exception\\ClientException))
#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#20 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#21 {main}
[previous exception] [object] (GuzzleHttp\\Exception\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)
[stacktrace]
#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\Exception\\RequestException::create(Object(GuzzleHttp\\Psr7\\Request), Object(GuzzleHttp\\Psr7\\Response), NULL, Array, NULL)
#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\Middleware::GuzzleHttp\\{closure}(Object(GuzzleHttp\\Psr7\\Response))
#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\Promise\\Promise::callHandler(1, Object(GuzzleHttp\\Psr7\\Response), NULL)
#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\Promise\\Promise::GuzzleHttp\\Promise\\{closure}()
#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\Promise\\TaskQueue->run(true)
#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\Promise\\Promise->invokeWaitFn()
#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\Promise\\Promise->waitIfPending()
#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\Promise\\Promise->invokeWaitList()
#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\Promise\\Promise->waitIfPending()
#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\Promise\\Promise->wait()
#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\Client->request('POST', 'https://api.hub...', Array)
#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#30 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#31 {main}
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:39] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
Sync Changes
Hide This Notification
Code changed:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4561
|
166
|
27
|
2026-05-07T14:09:21.447021+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162961447_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7673782238848625796
|
-8646559087753982588
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
PhostormVIew Project: faVsco.js, menu
master, menu
PhostormVIewINavigareCodeLaravelKeractorFV faVsco.js?9 masterProiectRateLimitException.php(C) HubsnotWehhookR.v D PaginationC Huospotraginations© PaginationConfig.phPaginationstate.ong> 0 ProspectSearchStratec> D Redisv D ServiceTraits(* OpportunitySyncTrav synccrmEntitiestra@ SyncFieldsTrait.phpT.Writecrmtrait.oho•DUulsWeohook© BatchSyncCollector.ph(C) BatchSvncRedisServicaC) Client.oho(C) ClosedDealStadesServDealFieldsService.php© DecorateActivity.php© FieldDefinitions.php© FieldTypeConverter.ph© HubspotClientinterfacec) Hubsoot TokenManage© PayloadBuilder.php© RemoteCrmObjectMan0 ResnonseNormalize.ph(e) Corvico nhnl© SyncFieldAction.php© SyncRelatedActivityMa© WebhookSyncBatchPrcv C IntegrationApp> D Accessors> DApicontioMDTOFiltersM.lobsProspectSearchStratec)M Service TraitsT IntegrationApp/.../SyncCrmEntitiesTrait.phpbasicapl.onpTImportBatchJobTrait.phg© MatchActivityCrmData.phpw Hubspot//syncermenutiestrait.onoclass Cllent extends Baseclient 1mpLements Hubspotclientintertaceorivate tunctionexecuterequest callable saocauol tSyternalManstirait.n( InternalAccountsMe143144? LavoutTrait nhnT MatchProspectsTraT. NotSunnortedTrait.t( SvncCrmEntitiesTra146M. SuncCrmFieldsTraitT CuneCrmMotadataTiT SystemStateTrait.pl© DataClient.php© DecorateActivity.phpthrow new RateLimitexception( message: 'Hubspot returned 429', SretryAfter, Se);throw $e;2 usagespublic function isHubspotRateLimit(Throwable $e): boolod: 'qetCode') && (int) $e->getCode === 429:Z usagespublic function barseRetryAfter(Throwable Se): intLumanate sumport racadesLoo::channelu channel: 'custom channel")->intol "parseretrvAtter")*1f (method exists(SenseHeaders')) {Sheaders = $e->getResponseHeaders() ?: [];$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;if (is arrav(Svalue)) {= Svaluel 0l ?? nullif (is numenic(Svalue)) ₫returnGint) Svalue:Scurrent = $e;uhiile Scunnent 1== null)if (method_exists($current,method: 'getResponse')) {Sresponse = $current->getResponseO:if (Sresponse !== null) {$headers = $response->getHeadersO:Saunnont - Caunnont.sao+Dnousoue?).Sthis->loq->info('[Hubspot] DEBUG Getting headers'. ['headers' => Sheaders 22 [1.1:arAuhe foa ids suadestiionsa Detect more secwritvlissues lin vour D!Dfflles //lTin SonarAube Cloud for free //lDownload SonarOmbe Server Illear more //lDonit ask adain /itodav 105251HubspotException.php© SyncOpportunity.phpProviderkateLimiter.onpA2 470 M2 A V1111Lukas/Stefka 121 - in 21 mInu / May 1/.09-21= custom.log« SF [jiminny@localhost]HS_local (jiminny@localhost]& console [PROD)& console [STAGING]0 0040~ 168[2026-05-07 14:08:22] local.NOTICE: Monitoring start[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512" "trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}[2026-05-07 14:08:31] local.INF0: Jiminny\Console \Commands\Command::run Memory usage before starting command {"cand". "malLdox. sk1p-u1stS.rerresh", "пепогуbетorесоmи[2026-05-07 14:08:31] local.INF0:usage ror commana ""conmand". "marl![2026-05-07 14:08:34] local.INF0: [SocialAccountServicel Fetching token {"socialAccountId":1499 "provider": "hubspot"} {"dorrelacion 10. aszoaata-ssya-4zc1-otos-Useda/t[2026-05-07 14:08:341 Local.INF0: [SocialAccountServicel Token retnieved {"socialAccountId":1499, "providen": "hubspot"} {"concelation id":"d326aafa-339a-42c1-bf53-05ebd72026-05-07 14:08:34 Local.INFU:EncryptedTokenManager Generating access token.• ""mode": "Leqacy", *"correlation 1d":"d526aata-359a-42C1-bt55-05ebd/t52782", "trace 10"[2026-05-07 14:08:34] local.INFO-":148,"team_id":2} {"correlation_id":"d3!2826-05-07 14:08:35 Local.INFU:[2026-05-07 14:08:35] Local. INFO:IHUbSpor DEBUG certng headens wneadensia watera wnur oylay 2026H08H56 rontent-yoema waool cat on asonHchanseteu %i-810t[Hubspot] Received 429 from API {"team_id":2."config_id":2."retry_after":10,"reason". "Client error›EaWs a us wa weron wra"messade wa wyou have reacher Your secondLy M tAalA WeNNON yDel Wa MRALeIAwr lConceLaiiion Cawa wonreoci-dtruncatedra"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782", "trace_id" :"19b29b52-6663-4853-8096-e32bf6618df7"}-[stacktracel::home/E6iminnw/ann//Servitces/Nem/A:Whs0of7/Pacinattlon/AUhsnofPacinatstonSacv.lce.oholesPa/R.J5mInnv.UKSenvilces.WTMcm.W:MhsoofaPacInatston.W:WhsnotlPaciinatstonsenvitce->execurtesear#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny|\Services|\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(0bject(Jiminn#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\|Services\\Crm\\Hubspot\\Service->matchByName('Robot')/home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\Console\\Commands\JiminnyDebugCommand->rateLimitO#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\Jol#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\Container\\BoundMethod::Illuminate\\Container\\{closure}O#7/home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))#8/home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate|Container|BoundMethod::callBoundMethod(Object(IlluminatellFoundation#9/home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminatel\ContainerBoundMethod::call(Object(Illuminatel FoundationApplicatio#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)#11/home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminatel|Console|Command->execute(0bject(SymfonyComponent|Console\Input|\ArgvInput), Object(Il#12/home/jiminny/vendor/Laravel/fnamework/src/Illuminate/Console/Command.php(180): SymfonyComponent||Consolel|Command\|Command->run(Obriect(SymfonyComponentl|Console#13/home/iiminny/vendor/symfony/console/Application.php(1117): Illuminatel|Consolel\Command->run(Obiect(SymfonyComponent||Consolel\Input\\ArgvInput), Obiect(Symfonyl1#14 /home/iiminny/vendor/symfony/console/Application.php(356): Symfony|Component||ConsolellApplication->doRunCommand(Obiect(Jiminny|Consolel\Commands|1JiminnyDebugCommEas homeraamannyvendor symtony/ consoLeyADolacation. Dho Ci95H syntonyauvonponenta consoLe ADo lucation->dokun ubnect syntony u vomponentaa consoLe uinout lArgvanouon Ubi#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Co#17/home/iiminny/vendon/Laravel/framework/src/IlLuminate/Foundation/Application.pho(1235): Illuminatel|Foundationl|Consolel|Kennel->handLe(0biect(SymfonylComponenti1Co#18 /home/jiminny/artisan(13): Illuminatel\Foundation\Application->handleCommand(Object(Symfony' \Component|\Consolel(Inputl\ArgvInput))#19 Smain?IDPeyious excentoNObeCISAVeNShONES WWOSOOt WEXCeoti onsBAdReoues Woooe ự5n O on aA sencen nesn 1e{|"status) "•\"error) " ("message) "•|"You have reached vour secondly Limit.|" ("errorTvpe)"•\"RATE LIMIT\ " |"correlationId\ "-|"019e02c4-d (truncated...)at /home/iiminny/vendor/hubsoot/hubsoot-oho/src/Excentions/HubsootExceotion.ono:241.stacktrace]lFAIOIESE6I1EI1W6eNAoeIS90322119GSe#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', '[URL_WITH_CREDENTIALS] Mubsnotiient->aetPadinatedDataAnnav#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny|\Services\\Crm\\Hubspot\\Service->matchByName('Robot')home/fliminny/ann/Ponsole/Commands/AiminnvDehugCommandLnhn.Ca.JiminnvRonsoleCommandsWiminnvDehuakommand->natelimito/home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\(Jobs\|JobThome/EEminaw/Wondod/AneavellEaamowodWced/Mllmminato/Contained/Mzlnhn/0DhhminatalWontainoaIBoundMothodbatlltm.natallontaiinoalEfailnstnaTa#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminatel|Container|\Util::unwrapIfClosure(Obiect(Closure))#10/home/iiminny/vendon/Laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminatel|Container||BoundMethod::callBoundMethod(Obriect(IlluminatellFoundatioW Windsurf Teaio 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4562
|
165
|
29
|
2026-05-07T14:09:26.664869+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162966664_m1.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4531659956596134304
|
6378757192794638436
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
4560
|
NULL
|
NULL
|
NULL
|
|
4563
|
166
|
28
|
2026-05-07T14:09:26.708060+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162966708_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"bounds":{"left":0.9630984,"top":0.07581804,"width":0.00930851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.4524601,"height":0.9066241},"on_screen":true,"value":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.48238033,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"bounds":{"left":0.49235374,"top":0.17478053,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.5043218,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4531659956596134304
|
6378757192794638436
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
4561
|
NULL
|
NULL
|
NULL
|
|
4564
|
165
|
30
|
2026-05-07T14:09:41.803308+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162981803_m1.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{
\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9180451576187180380
|
5225835688187529316
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{
\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4565
|
166
|
29
|
2026-05-07T14:09:41.888528+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162981888_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{
\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"bounds":{"left":0.9630984,"top":0.07581804,"width":0.00930851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.4524601,"height":0.9066241},"on_screen":true,"lines":[{"char_start":1127,"char_count":255,"bounds":{"left":0.5475399,"top":0.0,"width":0.4524601,"height":0.014365523}},{"char_start":1382,"char_count":101,"bounds":{"left":0.5475399,"top":0.0,"width":0.25930852,"height":0.014365523}},{"char_start":1483,"char_count":106,"bounds":{"left":0.5475399,"top":0.0,"width":0.27493352,"height":0.014365523}}],"value":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.48238033,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"bounds":{"left":0.49235374,"top":0.17478053,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.5043218,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
9180451576187180380
|
5225835688187529316
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{
\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4566
|
166
|
30
|
2026-05-07T14:09:47.072735+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162987072_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782",
"trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,
07 May 2026 14:08:35 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Content-Length":["227"],
"Connection":["keep-alive"],
"CF-Ray":["9f80cc29fd66dc1a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\",
cfr;desc=\"9f80cc2a06d23402-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],
"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu,
07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782",
"trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"bounds":{"left":0.9630984,"top":0.07581804,"width":0.00930851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\n\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,\n 07 May 2026 14:08:35 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Content-Length\":[\"227\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\",\n cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\n \"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu,\n 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\n\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.4524601,"height":0.9066241},"on_screen":true,"lines":[{"char_start":256,"char_count":29,"bounds":{"left":0.5475399,"top":0.0,"width":0.07247341,"height":0.014365523}},{"char_start":285,"char_count":52,"bounds":{"left":0.5475399,"top":0.0,"width":0.13198139,"height":0.014365523}},{"char_start":337,"char_count":27,"bounds":{"left":0.5475399,"top":0.0,"width":0.06715426,"height":0.014365523}},{"char_start":364,"char_count":30,"bounds":{"left":0.5475399,"top":0.0,"width":0.07513298,"height":0.014365523}}],"value":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\n\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,\n 07 May 2026 14:08:35 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Content-Length\":[\"227\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\",\n cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\n \"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu,\n 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\n\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.48238033,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"bounds":{"left":0.49235374,"top":0.17478053,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.5043218,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8884708740122144967
|
6378757184204703844
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782",
"trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,
07 May 2026 14:08:35 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Content-Length":["227"],
"Connection":["keep-alive"],
"CF-Ray":["9f80cc29fd66dc1a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\",
cfr;desc=\"9f80cc2a06d23402-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],
"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu,
07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782",
"trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
4565
|
NULL
|
NULL
|
NULL
|
|
4567
|
165
|
31
|
2026-05-07T14:09:47.158551+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778162987158_m1.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782",
"trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,
07 May 2026 14:08:35 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Content-Length":["227"],
"Connection":["keep-alive"],
"CF-Ray":["9f80cc29fd66dc1a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\",
cfr;desc=\"9f80cc2a06d23402-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],
"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu,
07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782",
"trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\n\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,\n 07 May 2026 14:08:35 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Content-Length\":[\"227\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\",\n cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\n \"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu,\n 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\n\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:08:35] local.INFO: parseRetryAfter {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\n\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,\n 07 May 2026 14:08:35 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Content-Length\":[\"227\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\",\n cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\n \"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu,\n 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\n\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8884708740122144967
|
6378757184204703844
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
17
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:35] local.INFO: parseRetryAfter {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782",
"trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,
07 May 2026 14:08:35 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Content-Length":["227"],
"Connection":["keep-alive"],
"CF-Ray":["9f80cc29fd66dc1a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\",
cfr;desc=\"9f80cc2a06d23402-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],
"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu,
07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782",
"trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
4564
|
NULL
|
NULL
|
NULL
|
|
4568
|
165
|
32
|
2026-05-07T14:10:13.113649+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163013113_m1.jpg...
|
PhpStorm
|
faVsco.js – SF [jiminny@localhost]
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
20
1
17
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
Sync Changes
Hide This Notification
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"20","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"17","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3351688021577348124
|
2218617716114593357
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
20
1
17
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
Sync Changes
Hide This Notification
Code changed:
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4569
|
166
|
31
|
2026-05-07T14:10:13.018627+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163013018_m2.jpg...
|
PhpStorm
|
faVsco.js – SF [jiminny@localhost]
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
20
1
17
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.5299202,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.53856385,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.54953456,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.5581782,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.5668218,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.5777925,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.5887633,"top":0.074221864,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.61535907,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.6263298,"top":0.074221864,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9587766,"top":0.074221864,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"20","depth":4,"bounds":{"left":0.9215425,"top":0.09896249,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.9338431,"top":0.09896249,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"17","depth":4,"bounds":{"left":0.9431516,"top":0.09896249,"width":0.00930851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.9544548,"top":0.09896249,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.9644282,"top":0.09896249,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09736632,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09736632,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.48238033,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"bounds":{"left":0.49235374,"top":0.17478053,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.5043218,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-341411725151059323
|
6686716930439912005
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
20
1
17
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4570
|
165
|
33
|
2026-05-07T14:10:15.082269+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163015082_m1.jpg...
|
PhpStorm
|
faVsco.js – SF [jiminny@localhost]
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
20
1
17
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"20","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"17","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;","depth":4,"on_screen":true,"value":"SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o\nJOIN activities a ON o.id = a.opportunity_id\nWHERE a.crm_configuration_id = 39\nAND a.actual_start_time > '2025-10-13'\nAND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM activities\nWHERE crm_configuration_id = 39 and user_id = 143\nand actual_start_time >= '2025-10-13'\nAND type IN ('conference', 'softphone-inbound', 'softphone-outbound')\n;\n\nSELECT * FROM opportunities WHERE account_id IN (178);\nselect * from activities where id IN (620137, 620187, 620188, 620189, 620230);\n\n# HS\nSELECT * FROM opportunities WHERE id IN (238);\nselect * from activities where id IN (477,2076);\n\nselect * from users;\n\nSELECT COUNT(*) FROM users;\nSELECT COUNT(*) FROM activities;\nSELECT COUNT(*) FROM opportunities;\n\nUPDATE activities\nSET\n actual_start_time = '2025-12-19 09:00:00',\n actual_end_time = '2025-12-19 10:30:00',\n scheduled_start_time = '2025-12-19 09:00:00',\n scheduled_end_time = '2025-12-19 10:30:00'\nWHERE id IN (407509,407375);\n\nselect * from partners;\n\nSELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id\nFROM activities\nWHERE user_id = 143\nAND actual_start_time >= '2025-10-13 00:00:00'\nAND actual_start_time <= '2026-01-13 23:59:59'\nORDER BY actual_start_time DESC;\n\nSELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;\nSELECT * FROM crm_layouts where crm_configuration_id = 39;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;\n# lead_id\n# account_id 177\n# contact_id 3969\n# opportunity_id\n# stage_id 203\n\nSELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;\n\nSELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'\nAND user_id = 143 and actual_start_time >= '2025-10-13';\n\nSELECT * FROM activities a\n# JOIN opportunities o ON a.opportunity_id = o.id\nWHERE a.crm_configuration_id = 39 AND a.type = 'conference'\nand status = 'completed' and recording_state = 'recorded'\nand a.actual_start_time >= '2025-10-13'\nAND a.user_id = 143\n;\n\nselect * from leads\nwhere crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707\n\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);\nSELECT * FROM activities WHERE id IN (356013,616188,616202,616310);\nSELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198\nSELECT * FROM activities WHERE id IN (356001, 356008); # contacts:\n\nSELECT * FROM opportunities WHERE id IN (1707);\nSELECT * FROM stages where id IN (204, 198);\nSELECT * FROM opportunities WHERE account_id IN (178);\nSELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';\nSELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal\n\nSELECT * FROM activities where crm_configuration_id = 39\nAND opportunity_id IS NULL\nAND is_internal = false\nand status = 'completed' and recording_state = 'recorded'\nAND actual_start_time >= '2025-10-13'\nAND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)\n# AND lead_id IN (112, 109)\n;\n\nSELECT * FROM crm_profiles WHERE user_id = 143;\n\nselect * from inboxes; # 212\nselect * from users where id = 143; # 143\nselect * from inbox_email_batches where inbox_id = 212\nand updated_at >= '2026-01-28 00:00:00' order by id desc;\nselect * from inbox_emails where inbox_id = 212\nand batch_id = 95885 order by id desc;\nselect * from email_messages where origin_user_id = 143;\nselect * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';\nselect * from participants where activity_id = 620247;\n\nselect * from crm_profiles where user_id = 143;\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001\nselect * from transcription where activity_id = 356001; # 6943\nselect * from ai_prompts where transcription_id = 6943;\nSELECT * FROM activity_summary_logs where activity_id = 356001;\n\nSELECT * FROM social_accounts WHERE sociable_id = 143;\n\n# ************************************************************************************\nSELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;\n# 422515 softphone tr. 8100\n\nSELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;\n# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS\n\nselect * from ai_prompts where transcription_id IN (8100, 7670);\nselect * from activity_summary_logs where activity_id = 407509;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nSELECT * FROM contacts WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\nSELECT * FROM leads WHERE crm_configuration_id = 39 and email = 'm.kogoj@gmx.at';\n\nSELECT * FROM activity_searches where user_id = 143;\nSELECT * FROM groups where team_id = 1;\n\nselect * from teams where id = 1;\nselect * from groups where team_id = 1; # 1150 - 7e75f8025c22\nselect id, name, group_id, status, deleted_at, email\nfrom users where team_id = 1 order by group_id desc ;\n\nselect * from activity_searches where id in (1977, 1978, 1979);\nselect * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);\nselect * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277\nselect * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879\n\nINSERT INTO `activity_search_filters`\n(`activity_search_id`, `filter`, `value`) VALUES\n(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),\n(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')\n;\n\nselect * from crm_configurations where id = 39;\n\n\nselect sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id\nwhere u.team_id = 1;\nSELECT * FROM social_accounts WHERE sociable_id = 1635;\nSELECT * FROM users WHERE id = 1635;\n\nselect * from teams where id = 1;\nselect * from users where team_id = 1;\nselect * from team_features where team_id = 1;\nselect * from features;\n\nSELECT * FROM activity_searches where id = 1982; # 1981\nSELECT * FROM activity_search_filters WHERE activity_search_id = 1982;\n\nSELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;\nSELECT * FROM groups WHERE id = 1439;\nSELECT * FROM users WHERE group_id = 1439;\n\nselect * from permissions; # 158\nselect * from roles;\nselect * from permission_role;\n\nselect * from teams where id = 1;\nselect * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;\nselect * from groups where id = 28;\nselect * from playbooks where team_id = 1;\nselect * from playbooks where id = 179;\nselect * from playbook_categories where id = 1391;\nselect * from users where id = 143;\nselect * from crm_profiles where user_id = 143;\nselect * from activities where crm_configuration_id = 39 and type = 'conference'\nand crm_provider_id IS NOT NULL ORDER by id desc;\nselect * from activities where id = 422003; # 00UO400000pB6fpMAC\n\nSELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type\nFROM automated_report_results ar\nJOIN automated_reports a ON a.id = ar.report_id\nWHERE a.type = 'ask_jiminny'\nLIMIT 10;\n\nSELECT * FROM automated_reports where id = 71;\nSELECT * FROM automated_report_results where report_id = 71;\nUPDATE automated_reports set playbook_categories = NULL where id = 68;\nSELECT * FROM automated_report_results where id = 275;\n\nSELECT * FROM automated_reports order by id desc;\nSELECT * FROM automated_report_results order by id desc;\nselect * from activity_searches where user_id = 143;\nselect * from ask_anything_prompts;\n\nSELECT `automated_report_results`.* FROM `automated_report_results`\nINNER JOIN `automated_reports`\n ON `automated_report_results`.`report_id` = `automated_reports`.`id`\nWHERE 1=1\n AND `automated_report_results`.`generated_at` IS NOT NULL\n# AND `automated_report_results`.`sent_at` IS NOT NULL\n AND `automated_reports`.`team_id` = 1\n AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$.\"users\"')\n;\n\nSELECT * FROM automated_reports where id = 67;\nSELECT * FROM automated_reports where id = 42;\nSELECT * FROM users WHERE id = 143; # group 28\n\nselect * from teams where id = 3143;\nselect * from crm_configurations where id = 500;\nselect * from users where name = 'Integration Account'; # 1695\nSELECT * FROM social_accounts WHERE sociable_id = 1695;\n\nselect * from activities where crm_configuration_id = 39\nand recording_state = 'recorded' and duration > 60\nand status = 'completed' and actual_start_time >= '2025-12-01';\n\nSELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;\n\nselect * from leads;","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-341411725151059323
|
6686716930439912005
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
20
1
17
2
4
Previous Highlighted Error
Next Highlighted Error
SELECT a.id, a.uuid, a.actual_start_time, o.id, o.uuid FROM opportunities o
JOIN activities a ON o.id = a.opportunity_id
WHERE a.crm_configuration_id = 39
AND a.actual_start_time > '2025-10-13'
AND a.type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM activities
WHERE crm_configuration_id = 39 and user_id = 143
and actual_start_time >= '2025-10-13'
AND type IN ('conference', 'softphone-inbound', 'softphone-outbound')
;
SELECT * FROM opportunities WHERE account_id IN (178);
select * from activities where id IN (620137, 620187, 620188, 620189, 620230);
# HS
SELECT * FROM opportunities WHERE id IN (238);
select * from activities where id IN (477,2076);
select * from users;
SELECT COUNT(*) FROM users;
SELECT COUNT(*) FROM activities;
SELECT COUNT(*) FROM opportunities;
UPDATE activities
SET
actual_start_time = '2025-12-19 09:00:00',
actual_end_time = '2025-12-19 10:30:00',
scheduled_start_time = '2025-12-19 09:00:00',
scheduled_end_time = '2025-12-19 10:30:00'
WHERE id IN (407509,407375);
select * from partners;
SELECT id, uuid, type, actual_start_time, user_id, crm_configuration_id
FROM activities
WHERE user_id = 143
AND actual_start_time >= '2025-10-13 00:00:00'
AND actual_start_time <= '2026-01-13 23:59:59'
ORDER BY actual_start_time DESC;
SELECT * FROM activities WHERE uuid_to_bin('78eda160-3086-435f-88a5-bb0c71b6008d') = uuid;
SELECT * FROM crm_layouts where crm_configuration_id = 39;
SELECT * FROM crm_layout_entities WHERE crm_layout_id = 282;
# lead_id
# account_id 177
# contact_id 3969
# opportunity_id
# stage_id 203
SELECT * FROM opportunities WHERE opportunities.crm_configuration_id = id = 282;
SELECT * FROM activities where crm_configuration_id = 39 AND type = 'conference'
AND user_id = 143 and actual_start_time >= '2025-10-13';
SELECT * FROM activities a
# JOIN opportunities o ON a.opportunity_id = o.id
WHERE a.crm_configuration_id = 39 AND a.type = 'conference'
and status = 'completed' and recording_state = 'recorded'
and a.actual_start_time >= '2025-10-13'
AND a.user_id = 143
;
select * from leads
where crm_configuration_id = 39; # 112 -> ac. 178, 109 => op. 1707
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310,407509,407375,356001,356008);
SELECT * FROM activities WHERE id IN (356013,616188,616202,616310);
SELECT * FROM activities WHERE id IN (407509,407375); # leads: 112, 109 | status - 198
SELECT * FROM activities WHERE id IN (356001, 356008); # contacts:
SELECT * FROM opportunities WHERE id IN (1707);
SELECT * FROM stages where id IN (204, 198);
SELECT * FROM opportunities WHERE account_id IN (178);
SELECT * FROM opportunities WHERE crm_configuration_id = 39 AND created_at > '2025-01-01';
SELECT * FROM contacts WHERE account_id IN (178); # 4118 Musaibe, 4448 Ceco Personal
SELECT * FROM activities where crm_configuration_id = 39
AND opportunity_id IS NULL
AND is_internal = false
and status = 'completed' and recording_state = 'recorded'
AND actual_start_time >= '2025-10-13'
AND (lead_id IS NOT NULL OR contact_id IS NOT NULL OR account_id IS NOT NULL)
# AND lead_id IN (112, 109)
;
SELECT * FROM crm_profiles WHERE user_id = 143;
select * from inboxes; # 212
select * from users where id = 143; # 143
select * from inbox_email_batches where inbox_id = 212
and updated_at >= '2026-01-28 00:00:00' order by id desc;
select * from inbox_emails where inbox_id = 212
and batch_id = 95885 order by id desc;
select * from email_messages where origin_user_id = 143;
select * from activities where user_id = 143 and updated_at >= '2026-01-28 00:00:00';
select * from participants where activity_id = 620247;
select * from crm_profiles where user_id = 143;
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid; # 356001
select * from transcription where activity_id = 356001; # 6943
select * from ai_prompts where transcription_id = 6943;
SELECT * FROM activity_summary_logs where activity_id = 356001;
SELECT * FROM social_accounts WHERE sociable_id = 143;
# [PASSWORD_DOTS]
SELECT * FROM activities WHERE uuid_to_bin('0164a4fb-cb95-454e-9edd-4d804e4999bd') = uuid;
# 422515 softphone tr. 8100
SELECT * FROM activities WHERE uuid_to_bin('7520add8-8d87-41a5-98e5-fc4edf96f21e') = uuid;
# 407509 conference tr. 7670 crmId: 00UD1000002J9aTMAS
select * from ai_prompts where transcription_id IN (8100, 7670);
select * from activity_summary_logs where activity_id = 407509;
select * from sidekick_settings;
select * from default_activity_types;
SELECT * FROM contacts WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM leads WHERE crm_configuration_id = 39 and email = '[EMAIL]';
SELECT * FROM activity_searches where user_id = 143;
SELECT * FROM groups where team_id = 1;
select * from teams where id = 1;
select * from groups where team_id = 1; # 1150 - 7e75f8025c22
select id, name, group_id, status, deleted_at, email
from users where team_id = 1 order by group_id desc ;
select * from activity_searches where id in (1977, 1978, 1979);
select * from activity_search_filters where activity_search_id IN (1977, 1978, 1979);
select * from activity_search_filters where filter = 'group_id' and value = '443f26b8-8512-437e-a9f9-7e75f8025c22'; # 10268, 10272, 10277
select * from nudges where activity_search_id IN (1977, 1978, 1979); # 877, 878, 879
INSERT INTO `activity_search_filters`
(`activity_search_id`, `filter`, `value`) VALUES
(1977, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1978, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22'),
(1979, 'group_id', '443f26b8-8512-437e-a9f9-7e75f8025c22')
;
select * from crm_configurations where id = 39;
select sa.* from users u JOIN social_accounts sa on u.id = sa.sociable_id
where u.team_id = 1;
SELECT * FROM social_accounts WHERE sociable_id = 1635;
SELECT * FROM users WHERE id = 1635;
select * from teams where id = 1;
select * from users where team_id = 1;
select * from team_features where team_id = 1;
select * from features;
SELECT * FROM activity_searches where id = 1982; # 1981
SELECT * FROM activity_search_filters WHERE activity_search_id = 1982;
SELECT * FROM activities WHERE uuid_to_bin('e916569b-086c-4bd1-94d7-5e3802c27ccf') = uuid;
SELECT * FROM groups WHERE id = 1439;
SELECT * FROM users WHERE group_id = 1439;
select * from permissions; # 158
select * from roles;
select * from permission_role;
select * from teams where id = 1;
select * from groups g JOIN playbooks p on g.playbook_id = p.id where g.team_id = 1;
select * from groups where id = 28;
select * from playbooks where team_id = 1;
select * from playbooks where id = 179;
select * from playbook_categories where id = 1391;
select * from users where id = 143;
select * from crm_profiles where user_id = 143;
select * from activities where crm_configuration_id = 39 and type = 'conference'
and crm_provider_id IS NOT NULL ORDER by id desc;
select * from activities where id = 422003; # 00UO400000pB6fpMAC
SELECT ar.id, ar.uuid, ar.media_type, ar.status, a.type
FROM automated_report_results ar
JOIN automated_reports a ON a.id = ar.report_id
WHERE a.type = 'ask_jiminny'
LIMIT 10;
SELECT * FROM automated_reports where id = 71;
SELECT * FROM automated_report_results where report_id = 71;
UPDATE automated_reports set playbook_categories = NULL where id = 68;
SELECT * FROM automated_report_results where id = 275;
SELECT * FROM automated_reports order by id desc;
SELECT * FROM automated_report_results order by id desc;
select * from activity_searches where user_id = 143;
select * from ask_anything_prompts;
SELECT `automated_report_results`.* FROM `automated_report_results`
INNER JOIN `automated_reports`
ON `automated_report_results`.`report_id` = `automated_reports`.`id`
WHERE 1=1
AND `automated_report_results`.`generated_at` IS NOT NULL
# AND `automated_report_results`.`sent_at` IS NOT NULL
AND `automated_reports`.`team_id` = 1
AND JSON_CONTAINS(`automated_reports`.`recipients`, 143, '$."users"')
;
SELECT * FROM automated_reports where id = 67;
SELECT * FROM automated_reports where id = 42;
SELECT * FROM users WHERE id = 143; # group 28
select * from teams where id = 3143;
select * from crm_configurations where id = 500;
select * from users where name = 'Integration Account'; # 1695
SELECT * FROM social_accounts WHERE sociable_id = 1695;
select * from activities where crm_configuration_id = 39
and recording_state = 'recorded' and duration > 60
and status = 'completed' and actual_start_time >= '2025-12-01';
SELECT * FROM activities WHERE uuid_to_bin('458cf915-b914-4000-b083-5687b32b2956') = uuid;
select * from leads;
Sync Changes
Hide This Notification
Code changed:
Hide
2
70
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
4568
|
NULL
|
NULL
|
NULL
|
|
4571
|
166
|
32
|
2026-05-07T14:10:16.612166+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163016612_m2.jpg...
|
PhpStorm
|
faVsco.js – HS_local [jiminny@localhost]
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.5299202,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.53856385,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.54953456,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.5581782,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.5668218,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.5777925,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.5887633,"top":0.074221864,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.61535907,"top":0.074221864,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.6263298,"top":0.074221864,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.9587766,"top":0.074221864,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.94514626,"top":0.09896249,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.95511967,"top":0.09896249,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.9644282,"top":0.09896249,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.09736632,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.09736632,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.48238033,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"bounds":{"left":0.49235374,"top":0.17478053,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.5043218,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3261207948425834308
|
6684458809013180516
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
4569
|
NULL
|
NULL
|
NULL
|
|
4572
|
165
|
34
|
2026-05-07T14:10:16.704580+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163016704_m1.jpg...
|
PhpStorm
|
faVsco.js – HS_local [jiminny@localhost]
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3261207948425834308
|
6684458809013180516
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4573
|
166
|
33
|
2026-05-07T14:10:21.424798+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163021424_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST [URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"168","depth":4,"bounds":{"left":0.96043885,"top":0.07581804,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring start {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring end {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.ERROR: [Aircall] Re-activating webhooks failed {\"team_id\":1,\"reason\":\"{\\\"message\\\":\\\"Forbidden\\\"}\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:40] local.INFO: [RetryFailedDownloads] Starting {\"options\":{\"from\":null,\"to\":null,\"help\":false,\"silent\":false,\"quiet\":false,\"verbose\":false,\"version\":false,\"ansi\":null,\"no-interaction\":false,\"env\":null}} {\"correlation_id\":\"3e4fee4c-1e25-454d-83aa-4fed507bc107\",\"trace_id\":\"e3fb6415-fa4e-4ea2-b163-9b09f456fb0c\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring start {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring end {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}","depth":4,"bounds":{"left":0.55019945,"top":0.0726257,"width":0.44980055,"height":0.9273743},"on_screen":true,"value":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring start {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring end {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.ERROR: [Aircall] Re-activating webhooks failed {\"team_id\":1,\"reason\":\"{\\\"message\\\":\\\"Forbidden\\\"}\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:40] local.INFO: [RetryFailedDownloads] Starting {\"options\":{\"from\":null,\"to\":null,\"help\":false,\"silent\":false,\"quiet\":false,\"verbose\":false,\"version\":false,\"ansi\":null,\"no-interaction\":false,\"env\":null}} {\"correlation_id\":\"3e4fee4c-1e25-454d-83aa-4fed507bc107\",\"trace_id\":\"e3fb6415-fa4e-4ea2-b163-9b09f456fb0c\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring start {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring end {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.48238033,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"bounds":{"left":0.49235374,"top":0.17478053,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.5043218,"top":0.17478053,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.51396275,"top":0.17318435,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.5212766,"top":0.17318435,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5314557014815799399
|
-2458563471524426716
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST [URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4574
|
165
|
35
|
2026-05-07T14:10:21.521266+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163021521_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST [URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"168","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring start {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring end {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.ERROR: [Aircall] Re-activating webhooks failed {\"team_id\":1,\"reason\":\"{\\\"message\\\":\\\"Forbidden\\\"}\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:40] local.INFO: [RetryFailedDownloads] Starting {\"options\":{\"from\":null,\"to\":null,\"help\":false,\"silent\":false,\"quiet\":false,\"verbose\":false,\"version\":false,\"ansi\":null,\"no-interaction\":false,\"env\":null}} {\"correlation_id\":\"3e4fee4c-1e25-454d-83aa-4fed507bc107\",\"trace_id\":\"e3fb6415-fa4e-4ea2-b163-9b09f456fb0c\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring start {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring end {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring start {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring end {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.ERROR: [Aircall] Re-activating webhooks failed {\"team_id\":1,\"reason\":\"{\\\"message\\\":\\\"Forbidden\\\"}\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:40] local.INFO: [RetryFailedDownloads] Starting {\"options\":{\"from\":null,\"to\":null,\"help\":false,\"silent\":false,\"quiet\":false,\"verbose\":false,\"version\":false,\"ansi\":null,\"no-interaction\":false,\"env\":null}} {\"correlation_id\":\"3e4fee4c-1e25-454d-83aa-4fed507bc107\",\"trace_id\":\"e3fb6415-fa4e-4ea2-b163-9b09f456fb0c\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring start {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring end {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info(\"parseRetryAfter\");\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n $current = $e;\n while ($current !== null) {\n if (method_exists($current, 'getResponse')) {\n $response = $current->getResponse();\n if ($response !== null) {\n $headers = $response->getHeaders();\n }\n }\n $current = $current->getPrevious();\n }\n\n $this->log->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n );\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5314557014815799399
|
-2458563471524426716
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
168
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST [URL_WITH_CREDENTIALS] CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
public function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info("parseRetryAfter");
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
$current = $e;
while ($current !== null) {
if (method_exists($current, 'getResponse')) {
$response = $current->getResponse();
if ($response !== null) {
$headers = $response->getHeaders();
}
}
$current = $current->getPrevious();
}
$this->log->info('[Hubspot] DEBUG Getting headers', [
'headers' => $headers ?? [],
]);
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
);
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log...
|
4572
|
NULL
|
NULL
|
NULL
|
|
4575
|
166
|
34
|
2026-05-07T14:10:22.835735+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163022835_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
221
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {"exception":"[object] (Jiminny\\Exceptions\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)
[stacktrace]
#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
[previous exception] [object] (SevenShores\\Hubspot\\Exceptions\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\Hubspot\\Exceptions\\HubspotException::create(Object(GuzzleHttp\\Exception\\ClientException))
#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#20 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#21 {main}
[previous exception] [object] (GuzzleHttp\\Exception\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)
[stacktrace]
#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\Exception\\RequestException::create(Object(GuzzleHttp\\Psr7\\Request), Object(GuzzleHttp\\Psr7\\Response), NULL, Array, NULL)
#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\Middleware::GuzzleHttp\\{closure}(Object(GuzzleHttp\\Psr7\\Response))
#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\Promise\\Promise::callHandler(1, Object(GuzzleHttp\\Psr7\\Response), NULL)
#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\Promise\\Promise::GuzzleHttp\\Promise\\{closure}()
#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\Promise\\TaskQueue->run(true)
#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\Promise\\Promise->invokeWaitFn()
#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\Promise\\Promise->waitIfPending()
#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\Promise\\Promise->invokeWaitList()
#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\Promise\\Promise->waitIfPending()
#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\Promise\\Promise->wait()
#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\Client->request('POST', 'https://api.hub...', Array)
#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#30 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#31 {main}
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:39] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:56] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"6918eda6-2b33-44f0-81b2-2b857d748d9d","trace_id":"b579f82f-2e2c-4f51-90c7-176a9cb4b034"}
[2026-05-07 14:08:56] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"6918eda6-2b33-44f0-81b2-2b857d748d9d","trace_id":"b579f82f-2e2c-4f51-90c7-176a9cb4b034"}
[2026-05-07 14:09:13] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"c74ba9d1-2bae-4193-bdc3-f28bb114a01a","trace_id":"b1119ae4-85e4-4811-a452-7f8bc17c9902"}
[2026-05-07 14:09:13] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"c74ba9d1-2bae-4193-bdc3-f28bb114a01a","trace_id":"b1119ae4-85e4-4811-a452-7f8bc17c9902"}
[2026-05-07 14:09:13] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"c74ba9d1-2bae-4193-bdc3-f28bb114a01a","trace_id":"b1119ae4-85e4-4811-a452-7f8bc17c9902"}
[2026-05-07 14:09:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"4f778d97-0b0b-4d22-811e-8e28826b49d2","trace_id":"0c888693-b171-4384-9c15-ee657feac714"}
[2026-05-07 14:09:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"4f778d97-0b0b-4d22-811e-8e28826b49d2","trace_id":"0c888693-b171-4384-9c15-ee657feac714"}
[2026-05-07 14:09:26] local.NOTICE: Monitoring start {"correlation_id":"27750001-5212-406e-bfb8-38a645dbcec5","trace_id":"57d40356-0102-42fc-ba6b-1ecc93535e1b"}
[2026-05-07 14:09:26] local.NOTICE: Monitoring end {"correlation_id":"27750001-5212-406e-bfb8-38a645dbcec5","trace_id":"57d40356-0102-42fc-ba6b-1ecc93535e1b"}
[2026-05-07 14:09:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fd08a346-b729-47e2-91d6-dc845103affb","trace_id":"dfb41a84-ecc1-4054-8517-dc4b2619f036"}
[2026-05-07 14:09:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fd08a346-b729-47e2-91d6-dc845103affb","trace_id":"dfb41a84-ecc1-4054-8517-dc4b2619f036"}
[2026-05-07 14:09:33] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"3e6c7142-56b7-415c-8661-e5494b4e60b1","trace_id":"b7b20129-94e3-4350-937e-42d64b87cb19"}
[2026-05-07 14:09:33] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"3e6c7142-56b7-415c-8661-e5494b4e60b1","trace_id":"b7b20129-94e3-4350-937e-42d64b87cb19"}
[2026-05-07 14:09:33] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"3e6c7142-56b7-415c-8661-e5494b4e60b1","trace_id":"b7b20129-94e3-4350-937e-42d64b87cb19"}
[2026-05-07 14:09:33] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"3e6c7142-56b7-415c-8661-e5494b4e60b1","trace_id":"b7b20129-94e3-4350-937e-42d64b87cb19"}
[2026-05-07 14:09:37] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"activity:aircall:check-and-renew","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1496,"provider":"aircall"} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1496,"provider":"aircall"} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.ERROR: [Aircall] Re-activating webhooks failed {"team_id":1,"reason":"{\"message\":\"Forbidden\"}"} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"activity:aircall:check-and-renew","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:40] local.INFO: [RetryFailedDownloads] Starting {"options":{"from":null,"to":null,"help":false,"silent":false,"quiet":false,"verbose":false,"version":false,"ansi":null,"no-interaction":false,"env":null}} {"correlation_id":"3e4fee4c-1e25-454d-83aa-4fed507bc107","trace_id":"e3fb6415-fa4e-4ea2-b163-9b09f456fb0c"}
[2026-05-07 14:10:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"6fdce905-c1fe-4707-ab21-d9861283b19f","trace_id":"5e3454bf-e096-4363-8668-a6fffd4f6249"}
[2026-05-07 14:10:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"6fdce905-c1fe-4707-ab21-d9861283b19f","trace_id":"5e3454bf-e096-4363-8668-a6fffd4f6249"}
[2026-05-07 14:10:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"6fdce905-c1fe-4707-ab21-d9861283b19f","trace_id":"5e3454bf-e096-4363-8668-a6fffd4f6249"}
[2026-05-07 14:10:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1ecbe999-9c82-450c-acf9-dc89bf054b42","trace_id":"96d0b27e-b838-4f8c-9959-25ec07ae5f6e"}
[2026-05-07 14:10:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1ecbe999-9c82-450c-acf9-dc89bf054b42","trace_id":"96d0b27e-b838-4f8c-9959-25ec07ae5f6e"}
[2026-05-07 14:10:08] local.NOTICE: Monitoring start {"correlation_id":"de73b6fd-8980-4497-aafb-d2c38c47d449","trace_id":"0ae3aae5-7abf-4911-84fb-2ba6537a5d38"}
[2026-05-07 14:10:08] local.NOTICE: Monitoring end {"correlation_id":"de73b6fd-8980-4497-aafb-d2c38c47d449","trace_id":"0ae3aae5-7abf-4911-84fb-2ba6537a5d38"}
[2026-05-07 14:10:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1b6708af-0228-4e34-88a3-288578822431","trace_id":"873e753a-20d2-4387-b9bc-d70791da1bff"}
[2026-05-07 14:10:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1b6708af-0228-4e34-88a3-288578822431","trace_id":"873e753a-20d2-4387-b9bc-d70791da1bff"}
[2026-05-07 14:10:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"df2612dc-5245-467e-8f65-27174c7d8c3b","trace_id":"5648bf8e-2e22-4731-80d3-bff5a73d628b"}
[2026-05-07 14:10:12] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"df2612dc-5245-467e-8f65-27174c7d8c3b","trace_id":"5648bf8e-2e22-4731-80d3-bff5a73d628b"}
[2026-05-07 14:10:12] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"df2612dc-5245-467e-8f65-27174c7d8c3b","trace_id":"5648bf8e-2e22-4731-80d3-bff5a73d628b"}
[2026-05-07 14:10:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"df2612dc-5245-467e-8f65-27174c7d8c3b","trace_id":"5648bf8e-2e22-4731-80d3-bff5a73d628b"}
[2026-05-07 14:10:15] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"9b0e7605-875e-4e51-83d6-2b89e4612ffc","trace_id":"39633b63-5f0d-4bc8-926c-320d50b6de7b"}
[2026-05-07 14:10:15] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {"correlation_id":"9b0e7605-875e-4e51-83d6-2b89e4612ffc","trace_id":"39633b63-5f0d-4bc8-926c-320d50b6de7b"}
[2026-05-07 14:10:15] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {"correlation_id":"9b0e7605-875e-4e51-83d6-2b89e4612ffc","trace_id":"39633b63-5f0d-4bc8-926c-320d50b6de7b"}
[2026-05-07 14:10:15] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"9b0e7605-875e-4e51-83d6-2b89e4612ffc","trace_id":"39633b63-5f0d-4bc8-926c-320d50b6de7b"}
[2026-05-07 14:10:18] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"activity:purge-stale","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7","trace_id":"0852729f-18b9-4c1f-8cf8-fa9772729628"}
[2026-05-07 14:10:18] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"activity:purge-stale","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7","trace_id":"0852729f-18b9-4c1f-8cf8-fa9772729628"}
[2026-05-07 14:10:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:text-relay:sync","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"8d1d9ffd-c346-4e05-bd33-2346f02967fa","trace_id":"a11335d7-a43f-4152-9008-4f7112aaba0d"}
[2026-05-07 14:10:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:text-relay:sync","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"8d1d9ffd-c346-4e05-bd33-2346f02967fa","trace_id":"a11335d7-a43f-4152-9008-4f7112aaba0d"}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"221","depth":4,"bounds":{"left":0.96077126,"top":0.07581804,"width":0.011635638,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring start {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring end {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.ERROR: [Aircall] Re-activating webhooks failed {\"team_id\":1,\"reason\":\"{\\\"message\\\":\\\"Forbidden\\\"}\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:40] local.INFO: [RetryFailedDownloads] Starting {\"options\":{\"from\":null,\"to\":null,\"help\":false,\"silent\":false,\"quiet\":false,\"verbose\":false,\"version\":false,\"ansi\":null,\"no-interaction\":false,\"env\":null}} {\"correlation_id\":\"3e4fee4c-1e25-454d-83aa-4fed507bc107\",\"trace_id\":\"e3fb6415-fa4e-4ea2-b163-9b09f456fb0c\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring start {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring end {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}","depth":4,"bounds":{"left":0.55019945,"top":0.0726257,"width":0.44980055,"height":0.9273743},"on_screen":true,"value":"[2026-05-07 14:08:22] local.NOTICE: Monitoring start {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:23] local.NOTICE: Monitoring end {\"correlation_id\":\"d080a9c0-0eca-4cb1-a3a8-0144debc5512\",\"trace_id\":\"b6fced05-bd66-448a-b6ed-1a158a4335f4\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fbe17093-d88a-4085-a766-e12ab1b648a4\",\"trace_id\":\"06b1c475-70e0-42ec-9805-d78f59c0c5d1\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {\"team_id\":2,\"config_id\":2,\"retry_after\":10,\"reason\":\"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {\"exception\":\"[object] (Jiminny\\\\Exceptions\\\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)\n[stacktrace]\n#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#19 {main}\n\n[previous exception] [object] (SevenShores\\\\Hubspot\\\\Exceptions\\\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)\n[stacktrace]\n#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\\\Hubspot\\\\Exceptions\\\\HubspotException::create(Object(GuzzleHttp\\\\Exception\\\\ClientException))\n#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#20 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#21 {main}\n\n[previous exception] [object] (GuzzleHttp\\\\Exception\\\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\\\"status\\\":\\\"error\\\",\\\"message\\\":\\\"You have reached your secondly limit.\\\",\\\"errorType\\\":\\\"RATE_LIMIT\\\",\\\"correlationId\\\":\\\"019e02c4-d (truncated...)\n at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)\n[stacktrace]\n#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\\\Exception\\\\RequestException::create(Object(GuzzleHttp\\\\Psr7\\\\Request), Object(GuzzleHttp\\\\Psr7\\\\Response), NULL, Array, NULL)\n#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\\\Middleware::GuzzleHttp\\\\{closure}(Object(GuzzleHttp\\\\Psr7\\\\Response))\n#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\\\Promise\\\\Promise::callHandler(1, Object(GuzzleHttp\\\\Psr7\\\\Response), NULL)\n#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\\\Promise\\\\Promise::GuzzleHttp\\\\Promise\\\\{closure}()\n#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\\\Promise\\\\TaskQueue->run(true)\n#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitFn()\n#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\\\Promise\\\\Promise->invokeWaitList()\n#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\\\Promise\\\\Promise->waitIfPending()\n#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\\\Promise\\\\Promise->wait()\n#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\\\Client->request('POST', 'https://api.hub...', Array)\n#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\\\Hubspot\\\\Http\\\\Client->request('POST', 'https://api.hub...', Array)\n#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), 'https://api.hub...', Array, Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\PaginationState))\n#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Pagination\\\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client), Array, 'contact', 0, 0, NULL)\n#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Client->getPaginatedData(Array, 'contact')\n#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\\\Services\\\\Crm\\\\Hubspot\\\\Service->matchByName('Robot')\n#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->rateLimit()\n#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand->handle(Object(Jiminny\\\\Jobs\\\\JobDispatcher), Object(Jiminny\\\\Services\\\\Kiosk\\\\AutomatedReports\\\\AutomatedReportsService), Object(Jiminny\\\\Repositories\\\\AutomatedReportsRepository), Object(Jiminny\\\\Services\\\\UserPilot\\\\UserPilotClient))\n#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\\\Container\\\\BoundMethod::Illuminate\\\\Container\\\\{closure}()\n#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\\\Container\\\\Util::unwrapIfClosure(Object(Closure))\n#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\\\Container\\\\BoundMethod::callBoundMethod(Object(Illuminate\\\\Foundation\\\\Application), Array, Object(Closure))\n#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\\\Container\\\\BoundMethod::call(Object(Illuminate\\\\Foundation\\\\Application), Array, Array, NULL)\n#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\\\Container\\\\Container->call(Array)\n#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\\\Console\\\\Command->execute(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\\\Component\\\\Console\\\\Command\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Illuminate\\\\Console\\\\OutputStyle))\n#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\\\Console\\\\Command->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\\\Component\\\\Console\\\\Application->doRunCommand(Object(Jiminny\\\\Console\\\\Commands\\\\JiminnyDebugCommand), Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\\\Component\\\\Console\\\\Application->doRun(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\\\Component\\\\Console\\\\Application->run(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\\\Foundation\\\\Console\\\\Kernel->handle(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput), Object(Symfony\\\\Component\\\\Console\\\\Output\\\\ConsoleOutput))\n#30 /home/jiminny/artisan(13): Illuminate\\\\Foundation\\\\Application->handleCommand(Object(Symfony\\\\Component\\\\Console\\\\Input\\\\ArgvInput))\n#31 {main}\n\"} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}\n[2026-05-07 14:08:39] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:40] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"0b0aaffc-b17d-4e58-b762-977c85cdd0b1\",\"trace_id\":\"2c3239ee-27ee-4562-b879-7185d83e71bd\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:44] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4a0b016f-33d1-4fe7-9614-a5866168a51a\",\"trace_id\":\"789fa62b-e244-461c-99c1-3cf1d571aee3\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:08:56] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6918eda6-2b33-44f0-81b2-2b857d748d9d\",\"trace_id\":\"b579f82f-2e2c-4f51-90c7-176a9cb4b034\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:13] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c74ba9d1-2bae-4193-bdc3-f28bb114a01a\",\"trace_id\":\"b1119ae4-85e4-4811-a452-7f8bc17c9902\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4f778d97-0b0b-4d22-811e-8e28826b49d2\",\"trace_id\":\"0c888693-b171-4384-9c15-ee657feac714\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring start {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:26] local.NOTICE: Monitoring end {\"correlation_id\":\"27750001-5212-406e-bfb8-38a645dbcec5\",\"trace_id\":\"57d40356-0102-42fc-ba6b-1ecc93535e1b\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"fd08a346-b729-47e2-91d6-dc845103affb\",\"trace_id\":\"dfb41a84-ecc1-4054-8517-dc4b2619f036\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:33] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"3e6c7142-56b7-415c-8661-e5494b4e60b1\",\"trace_id\":\"b7b20129-94e3-4350-937e-42d64b87cb19\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1496,\"provider\":\"aircall\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.ERROR: [Aircall] Re-activating webhooks failed {\"team_id\":1,\"reason\":\"{\\\"message\\\":\\\"Forbidden\\\"}\"} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:37] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:aircall:check-and-renew\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d7ec8872-1181-403f-8d43-1616114e02a7\",\"trace_id\":\"8b490662-7140-4875-880c-0b7ae10b4b83\"}\n[2026-05-07 14:09:40] local.INFO: [RetryFailedDownloads] Starting {\"options\":{\"from\":null,\"to\":null,\"help\":false,\"silent\":false,\"quiet\":false,\"verbose\":false,\"version\":false,\"ansi\":null,\"no-interaction\":false,\"env\":null}} {\"correlation_id\":\"3e4fee4c-1e25-454d-83aa-4fed507bc107\",\"trace_id\":\"e3fb6415-fa4e-4ea2-b163-9b09f456fb0c\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:05] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6fdce905-c1fe-4707-ab21-d9861283b19f\",\"trace_id\":\"5e3454bf-e096-4363-8668-a6fffd4f6249\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:06] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1ecbe999-9c82-450c-acf9-dc89bf054b42\",\"trace_id\":\"96d0b27e-b838-4f8c-9959-25ec07ae5f6e\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring start {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:08] local.NOTICE: Monitoring end {\"correlation_id\":\"de73b6fd-8980-4497-aafb-d2c38c47d449\",\"trace_id\":\"0ae3aae5-7abf-4911-84fb-2ba6537a5d38\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1b6708af-0228-4e34-88a3-288578822431\",\"trace_id\":\"873e753a-20d2-4387-b9bc-d70791da1bff\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:12] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"df2612dc-5245-467e-8f65-27174c7d8c3b\",\"trace_id\":\"5648bf8e-2e22-4731-80d3-bff5a73d628b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:15] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9b0e7605-875e-4e51-83d6-2b89e4612ffc\",\"trace_id\":\"39633b63-5f0d-4bc8-926c-320d50b6de7b\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:18] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"activity:purge-stale\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7\",\"trace_id\":\"0852729f-18b9-4c1f-8cf8-fa9772729628\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}\n[2026-05-07 14:10:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:text-relay:sync\",\"memoryBeforeCommandInMb\":62.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"8d1d9ffd-c346-4e05-bd33-2346f02967fa\",\"trace_id\":\"a11335d7-a43f-4152-9008-4f7112aaba0d\"}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
7840235707277393182
|
-2493465167907943259
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
221
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:08:22] local.NOTICE: Monitoring start {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:23] local.NOTICE: Monitoring end {"correlation_id":"d080a9c0-0eca-4cb1-a3a8-0144debc5512","trace_id":"b6fced05-bd66-448a-b6ed-1a158a4335f4"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fbe17093-d88a-4085-a766-e12ab1b648a4","trace_id":"06b1c475-70e0-42ec-9805-d78f59c0c5d1"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:35] local.INFO: [Hubspot] Received 429 from API {"team_id":2,"config_id":2,"retry_after":10,"reason":"Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:36] local.ERROR: Hubspot returned 429 {"exception":"[object] (Jiminny\\Exceptions\\RateLimitException(code: 0): Hubspot returned 429 at /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206)
[stacktrace]
#0 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#1 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#2 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#3 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#4 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#5 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#6 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#11 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#13 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#14 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#15 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#19 {main}
[previous exception] [object] (SevenShores\\Hubspot\\Exceptions\\BadRequest(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24)
[stacktrace]
#0 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(125): SevenShores\\Hubspot\\Exceptions\\HubspotException::create(Object(GuzzleHttp\\Exception\\ClientException))
#1 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#2 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#3 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#4 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#5 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#6 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#7 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#8 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#9 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#10 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#11 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#12 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#13 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#14 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#15 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#16 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#17 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#20 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#21 {main}
[previous exception] [object] (GuzzleHttp\\Exception\\ClientException(code: 429): Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:
{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e02c4-d (truncated...)
at /home/jiminny/vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:111)
[stacktrace]
#0 /home/jiminny/vendor/guzzlehttp/guzzle/src/Middleware.php(72): GuzzleHttp\\Exception\\RequestException::create(Object(GuzzleHttp\\Psr7\\Request), Object(GuzzleHttp\\Psr7\\Response), NULL, Array, NULL)
#1 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(209): GuzzleHttp\\Middleware::GuzzleHttp\\{closure}(Object(GuzzleHttp\\Psr7\\Response))
#2 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(158): GuzzleHttp\\Promise\\Promise::callHandler(1, Object(GuzzleHttp\\Psr7\\Response), NULL)
#3 /home/jiminny/vendor/guzzlehttp/promises/src/TaskQueue.php(52): GuzzleHttp\\Promise\\Promise::GuzzleHttp\\Promise\\{closure}()
#4 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(251): GuzzleHttp\\Promise\\TaskQueue->run(true)
#5 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(227): GuzzleHttp\\Promise\\Promise->invokeWaitFn()
#6 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(272): GuzzleHttp\\Promise\\Promise->waitIfPending()
#7 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(229): GuzzleHttp\\Promise\\Promise->invokeWaitList()
#8 /home/jiminny/vendor/guzzlehttp/promises/src/Promise.php(69): GuzzleHttp\\Promise\\Promise->waitIfPending()
#9 /home/jiminny/vendor/guzzlehttp/guzzle/src/Client.php(189): GuzzleHttp\\Promise\\Promise->wait()
#10 /home/jiminny/vendor/hubspot/hubspot-php/src/Http/Client.php(113): GuzzleHttp\\Client->request('POST', 'https://api.hub...', Array)
#11 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(166): SevenShores\\Hubspot\\Http\\Client->request('POST', 'https://api.hub...', Array)
#12 /home/jiminny/app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php(52): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), 'https://api.hub...', Array, Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))
#13 /home/jiminny/app/Services/Crm/Hubspot/Client.php(191): Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService->getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), Array, 'contact', 0, 0, NULL)
#14 /home/jiminny/app/Services/Crm/Hubspot/Service.php(1203): Jiminny\\Services\\Crm\\Hubspot\\Client->getPaginatedData(Array, 'contact')
#15 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(353): Jiminny\\Services\\Crm\\Hubspot\\Service->matchByName('Robot')
#16 /home/jiminny/app/Console/Commands/JiminnyDebugCommand.php(44): Jiminny\\Console\\Commands\\JiminnyDebugCommand->rateLimit()
#17 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): Jiminny\\Console\\Commands\\JiminnyDebugCommand->handle(Object(Jiminny\\Jobs\\JobDispatcher), Object(Jiminny\\Services\\Kiosk\\AutomatedReports\\AutomatedReportsService), Object(Jiminny\\Repositories\\AutomatedReportsRepository), Object(Jiminny\\Services\\UserPilot\\UserPilotClient))
#18 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Util.php(43): Illuminate\\Container\\BoundMethod::Illuminate\\Container\\{closure}()
#19 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(96): Illuminate\\Container\\Util::unwrapIfClosure(Object(Closure))
#20 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(35): Illuminate\\Container\\BoundMethod::callBoundMethod(Object(Illuminate\\Foundation\\Application), Array, Object(Closure))
#21 /home/jiminny/vendor/laravel/framework/src/Illuminate/Container/Container.php(799): Illuminate\\Container\\BoundMethod::call(Object(Illuminate\\Foundation\\Application), Array, Array, NULL)
#22 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(211): Illuminate\\Container\\Container->call(Array)
#23 /home/jiminny/vendor/symfony/console/Command/Command.php(341): Illuminate\\Console\\Command->execute(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#24 /home/jiminny/vendor/laravel/framework/src/Illuminate/Console/Command.php(180): Symfony\\Component\\Console\\Command\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Illuminate\\Console\\OutputStyle))
#25 /home/jiminny/vendor/symfony/console/Application.php(1117): Illuminate\\Console\\Command->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#26 /home/jiminny/vendor/symfony/console/Application.php(356): Symfony\\Component\\Console\\Application->doRunCommand(Object(Jiminny\\Console\\Commands\\JiminnyDebugCommand), Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#27 /home/jiminny/vendor/symfony/console/Application.php(195): Symfony\\Component\\Console\\Application->doRun(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#28 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(198): Symfony\\Component\\Console\\Application->run(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#29 /home/jiminny/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(1235): Illuminate\\Foundation\\Console\\Kernel->handle(Object(Symfony\\Component\\Console\\Input\\ArgvInput), Object(Symfony\\Component\\Console\\Output\\ConsoleOutput))
#30 /home/jiminny/artisan(13): Illuminate\\Foundation\\Application->handleCommand(Object(Symfony\\Component\\Console\\Input\\ArgvInput))
#31 {main}
"} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
[2026-05-07 14:08:39] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:39] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:40] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"0b0aaffc-b17d-4e58-b762-977c85cdd0b1","trace_id":"2c3239ee-27ee-4562-b879-7185d83e71bd"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:06:00, 2026-05-07 14:08:00] {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:44] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"4a0b016f-33d1-4fe7-9614-a5866168a51a","trace_id":"789fa62b-e244-461c-99c1-3cf1d571aee3"}
[2026-05-07 14:08:56] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"6918eda6-2b33-44f0-81b2-2b857d748d9d","trace_id":"b579f82f-2e2c-4f51-90c7-176a9cb4b034"}
[2026-05-07 14:08:56] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:retry-failed","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"6918eda6-2b33-44f0-81b2-2b857d748d9d","trace_id":"b579f82f-2e2c-4f51-90c7-176a9cb4b034"}
[2026-05-07 14:09:13] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"c74ba9d1-2bae-4193-bdc3-f28bb114a01a","trace_id":"b1119ae4-85e4-4811-a452-7f8bc17c9902"}
[2026-05-07 14:09:13] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"c74ba9d1-2bae-4193-bdc3-f28bb114a01a","trace_id":"b1119ae4-85e4-4811-a452-7f8bc17c9902"}
[2026-05-07 14:09:13] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"c74ba9d1-2bae-4193-bdc3-f28bb114a01a","trace_id":"b1119ae4-85e4-4811-a452-7f8bc17c9902"}
[2026-05-07 14:09:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"4f778d97-0b0b-4d22-811e-8e28826b49d2","trace_id":"0c888693-b171-4384-9c15-ee657feac714"}
[2026-05-07 14:09:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"4f778d97-0b0b-4d22-811e-8e28826b49d2","trace_id":"0c888693-b171-4384-9c15-ee657feac714"}
[2026-05-07 14:09:26] local.NOTICE: Monitoring start {"correlation_id":"27750001-5212-406e-bfb8-38a645dbcec5","trace_id":"57d40356-0102-42fc-ba6b-1ecc93535e1b"}
[2026-05-07 14:09:26] local.NOTICE: Monitoring end {"correlation_id":"27750001-5212-406e-bfb8-38a645dbcec5","trace_id":"57d40356-0102-42fc-ba6b-1ecc93535e1b"}
[2026-05-07 14:09:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"fd08a346-b729-47e2-91d6-dc845103affb","trace_id":"dfb41a84-ecc1-4054-8517-dc4b2619f036"}
[2026-05-07 14:09:31] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"fd08a346-b729-47e2-91d6-dc845103affb","trace_id":"dfb41a84-ecc1-4054-8517-dc4b2619f036"}
[2026-05-07 14:09:33] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"3e6c7142-56b7-415c-8661-e5494b4e60b1","trace_id":"b7b20129-94e3-4350-937e-42d64b87cb19"}
[2026-05-07 14:09:33] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"3e6c7142-56b7-415c-8661-e5494b4e60b1","trace_id":"b7b20129-94e3-4350-937e-42d64b87cb19"}
[2026-05-07 14:09:33] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"3e6c7142-56b7-415c-8661-e5494b4e60b1","trace_id":"b7b20129-94e3-4350-937e-42d64b87cb19"}
[2026-05-07 14:09:33] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"3e6c7142-56b7-415c-8661-e5494b4e60b1","trace_id":"b7b20129-94e3-4350-937e-42d64b87cb19"}
[2026-05-07 14:09:37] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"activity:aircall:check-and-renew","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1496,"provider":"aircall"} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1496,"provider":"aircall"} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.ERROR: [Aircall] Re-activating webhooks failed {"team_id":1,"reason":"{\"message\":\"Forbidden\"}"} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:37] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"activity:aircall:check-and-renew","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"d7ec8872-1181-403f-8d43-1616114e02a7","trace_id":"8b490662-7140-4875-880c-0b7ae10b4b83"}
[2026-05-07 14:09:40] local.INFO: [RetryFailedDownloads] Starting {"options":{"from":null,"to":null,"help":false,"silent":false,"quiet":false,"verbose":false,"version":false,"ansi":null,"no-interaction":false,"env":null}} {"correlation_id":"3e4fee4c-1e25-454d-83aa-4fed507bc107","trace_id":"e3fb6415-fa4e-4ea2-b163-9b09f456fb0c"}
[2026-05-07 14:10:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"6fdce905-c1fe-4707-ab21-d9861283b19f","trace_id":"5e3454bf-e096-4363-8668-a6fffd4f6249"}
[2026-05-07 14:10:05] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"6fdce905-c1fe-4707-ab21-d9861283b19f","trace_id":"5e3454bf-e096-4363-8668-a6fffd4f6249"}
[2026-05-07 14:10:05] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"6fdce905-c1fe-4707-ab21-d9861283b19f","trace_id":"5e3454bf-e096-4363-8668-a6fffd4f6249"}
[2026-05-07 14:10:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1ecbe999-9c82-450c-acf9-dc89bf054b42","trace_id":"96d0b27e-b838-4f8c-9959-25ec07ae5f6e"}
[2026-05-07 14:10:06] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1ecbe999-9c82-450c-acf9-dc89bf054b42","trace_id":"96d0b27e-b838-4f8c-9959-25ec07ae5f6e"}
[2026-05-07 14:10:08] local.NOTICE: Monitoring start {"correlation_id":"de73b6fd-8980-4497-aafb-d2c38c47d449","trace_id":"0ae3aae5-7abf-4911-84fb-2ba6537a5d38"}
[2026-05-07 14:10:08] local.NOTICE: Monitoring end {"correlation_id":"de73b6fd-8980-4497-aafb-d2c38c47d449","trace_id":"0ae3aae5-7abf-4911-84fb-2ba6537a5d38"}
[2026-05-07 14:10:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1b6708af-0228-4e34-88a3-288578822431","trace_id":"873e753a-20d2-4387-b9bc-d70791da1bff"}
[2026-05-07 14:10:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1b6708af-0228-4e34-88a3-288578822431","trace_id":"873e753a-20d2-4387-b9bc-d70791da1bff"}
[2026-05-07 14:10:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"df2612dc-5245-467e-8f65-27174c7d8c3b","trace_id":"5648bf8e-2e22-4731-80d3-bff5a73d628b"}
[2026-05-07 14:10:12] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"df2612dc-5245-467e-8f65-27174c7d8c3b","trace_id":"5648bf8e-2e22-4731-80d3-bff5a73d628b"}
[2026-05-07 14:10:12] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"df2612dc-5245-467e-8f65-27174c7d8c3b","trace_id":"5648bf8e-2e22-4731-80d3-bff5a73d628b"}
[2026-05-07 14:10:12] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"df2612dc-5245-467e-8f65-27174c7d8c3b","trace_id":"5648bf8e-2e22-4731-80d3-bff5a73d628b"}
[2026-05-07 14:10:15] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"9b0e7605-875e-4e51-83d6-2b89e4612ffc","trace_id":"39633b63-5f0d-4bc8-926c-320d50b6de7b"}
[2026-05-07 14:10:15] local.INFO: Running conference:monitor:count command for activities in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {"correlation_id":"9b0e7605-875e-4e51-83d6-2b89e4612ffc","trace_id":"39633b63-5f0d-4bc8-926c-320d50b6de7b"}
[2026-05-07 14:10:15] local.INFO: [conference:monitor:count] No activities found in (2026-05-07 14:08:00, 2026-05-07 14:10:00] {"correlation_id":"9b0e7605-875e-4e51-83d6-2b89e4612ffc","trace_id":"39633b63-5f0d-4bc8-926c-320d50b6de7b"}
[2026-05-07 14:10:15] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"conference:monitor:count","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"9b0e7605-875e-4e51-83d6-2b89e4612ffc","trace_id":"39633b63-5f0d-4bc8-926c-320d50b6de7b"}
[2026-05-07 14:10:18] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"activity:purge-stale","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7","trace_id":"0852729f-18b9-4c1f-8cf8-fa9772729628"}
[2026-05-07 14:10:18] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"activity:purge-stale","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f2528a96-6d20-45d2-8ad4-e68fdb1b03f7","trace_id":"0852729f-18b9-4c1f-8cf8-fa9772729628"}
[2026-05-07 14:10:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:text-relay:sync","memoryBeforeCommandInMb":62.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"8d1d9ffd-c346-4e05-bd33-2346f02967fa","trace_id":"a11335d7-a43f-4152-9008-4f7112aaba0d"}
[2026-05-07 14:10:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:text-relay:sync","memoryBeforeCommandInMb":62.0,"memoryAfterCommandInMB":62.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"8d1d9ffd-c346-4e05-bd33-2346f02967fa","trace_id":"a11335d7-a43f-4152-9008-4f7112aaba0d"}...
|
4573
|
NULL
|
NULL
|
NULL
|
|
4576
|
166
|
35
|
2026-05-07T14:10:28.805754+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163028805_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PostmanVIewWindowmelprTavsco.sProiectRematchActivi PostmanVIewWindowmelprTavsco.sProiectRematchActivityOnCr,= custom.logE hubspot-journal-poll.log© PaginationState.php© PaginationConfig.php© SyncOpportunitiesJob.php= laravel.log<)> phpunit.xmis ttt.js© ImportOpportunityBatch.phpTImportBatchJobTrait.pl© CachedCrmServiceDecorator.phc(* Hubspot/../Syncd= oauth-onivale.keyE oauth-public.kevE storageE supervisord.picclass Cllent extends Baseclient 1mpLenw text-relav.isonv D tests> Feature→intearation→ Services115116117118>M Actions• M Comoonent> M Confiauration• IM Consold• M Contracte122123h DomainD DTOD Enums• M Events.125D Exceptionsm fixtures> D GuardsM Holnors> C Http• C Intearations> C Interactionsv @Jobs>@ Activityi> C AiAutomation> M Audidv M AutomatedReports© CreateResultsTest.phoC RequestGenerateAsk Ji(C) RequestGenerateReoo© SendReportExpiringSocSendRenort.lobliest.oh( SendRenortMaillobTes@ SendRenortNotGenera140141144145› D Calendar>M Crm> M DealPicksM Mailhox)149• @ Streaming.)m Toamorivate tunction executerequesticathrow new RateLimitsxothrow $e;2 usagespublic function isHubspotRateLimitZ usagespublic function parseRetryAfter(ThLumanate support racades Lo1f (method exists(SeSvalue = Sheaders|'Retry-.if (is arrav(Svalue)) {= Svaluel0l?if Gs numeric(svalue)) «ceturn ant. Svalue:Scurrent = $e;whiile Scunnent 1== null.if (method_exists($currentSnecnonce = Ceunnent.if (Sresponse !== nul1Choadene = SnecnorSaunnont - Caunnont-sao+p.Sthis->loq->info('[Hubspot] DE'headers' => Sheaders 22 [C TelephonyMiicor(et ImnartD.1:$0: Lukas/Stefka 121 - in 20 m100% L2Thu 7 May 17:10:28• SearchUparadeYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissions to see who can edit, or upgrade to restore collaborationv COLLECtIONS> CRM Owners> CRM Pipelines• Dealsengagements> D OLD ENGAGEMENTSuer list meetingsGET read call> post soarch calleGET ist callsPOST meetings scheduledGET det meetinoPOST get link to task> POST Create Contact with Association› Hubspotvteration run HSv GET Read Copyeg. An error occurred.se. successful operationv lteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESCAPCHSEARCHIPost search contact by phonePOST search contact by emailpost coarch meetinaspost Search callc vaPOST Search related meetinas v3Post coarch deals> Ticketsv Uicofull> PoST filter per comoanv / onlv open deal stagesGet engagements old associated by dealments old associated by companvSustem Resource WarningSustem resources are constrained. Thesystem mav not be able to generate the loadeded for this test and the cest is likely toa Connect GitConcole 5.) TerminaPOST search de • POST Reada bal • POST Read a bat eGET ReadGET ReadGET read callGET Get EngageGET Read CopyGET httos:/lapi.rm lteration run HSPOST search contaPOSt search conta• Iteration run SD Iteration run SelNo environmentH7).u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileSummaryTotal requests sent ©6.550Requests/second ©Avg. response time ©P90 ©109.04155 ms173 msp95 O196 msP99 ©287 msError % ©0,00Failure % ©Peak CPU % ©Peak Memory % ©0,00100.0 %195 %Filter bv requestsvAva. response% 100262 ms 140 req/s25017•08.25|17-08-2117•08•2717•08•4617•08•4917-08-55)17•09•0117•09•0717.09-1217.09-19- Requests/second - Ava. response - Error % - Virtual users ..• CPU % ••• Memory %Dorformonss dotolle fortotol durotinnPOST search contact by email Copy6.550108.480.00Failure %0.00Resp. time (Ava. ms)Min (ms)Max ms)693287Globals Vault Tools?000...
|
NULL
|
4427944367271277315
|
NULL
|
visual_change
|
ocr
|
NULL
|
PostmanVIewWindowmelprTavsco.sProiectRematchActivi PostmanVIewWindowmelprTavsco.sProiectRematchActivityOnCr,= custom.logE hubspot-journal-poll.log© PaginationState.php© PaginationConfig.php© SyncOpportunitiesJob.php= laravel.log<)> phpunit.xmis ttt.js© ImportOpportunityBatch.phpTImportBatchJobTrait.pl© CachedCrmServiceDecorator.phc(* Hubspot/../Syncd= oauth-onivale.keyE oauth-public.kevE storageE supervisord.picclass Cllent extends Baseclient 1mpLenw text-relav.isonv D tests> Feature→intearation→ Services115116117118>M Actions• M Comoonent> M Confiauration• IM Consold• M Contracte122123h DomainD DTOD Enums• M Events.125D Exceptionsm fixtures> D GuardsM Holnors> C Http• C Intearations> C Interactionsv @Jobs>@ Activityi> C AiAutomation> M Audidv M AutomatedReports© CreateResultsTest.phoC RequestGenerateAsk Ji(C) RequestGenerateReoo© SendReportExpiringSocSendRenort.lobliest.oh( SendRenortMaillobTes@ SendRenortNotGenera140141144145› D Calendar>M Crm> M DealPicksM Mailhox)149• @ Streaming.)m Toamorivate tunction executerequesticathrow new RateLimitsxothrow $e;2 usagespublic function isHubspotRateLimitZ usagespublic function parseRetryAfter(ThLumanate support racades Lo1f (method exists(SeSvalue = Sheaders|'Retry-.if (is arrav(Svalue)) {= Svaluel0l?if Gs numeric(svalue)) «ceturn ant. Svalue:Scurrent = $e;whiile Scunnent 1== null.if (method_exists($currentSnecnonce = Ceunnent.if (Sresponse !== nul1Choadene = SnecnorSaunnont - Caunnont-sao+p.Sthis->loq->info('[Hubspot] DE'headers' => Sheaders 22 [C TelephonyMiicor(et ImnartD.1:$0: Lukas/Stefka 121 - in 20 m100% L2Thu 7 May 17:10:28• SearchUparadeYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissions to see who can edit, or upgrade to restore collaborationv COLLECtIONS> CRM Owners> CRM Pipelines• Dealsengagements> D OLD ENGAGEMENTSuer list meetingsGET read call> post soarch calleGET ist callsPOST meetings scheduledGET det meetinoPOST get link to task> POST Create Contact with Association› Hubspotvteration run HSv GET Read Copyeg. An error occurred.se. successful operationv lteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESCAPCHSEARCHIPost search contact by phonePOST search contact by emailpost coarch meetinaspost Search callc vaPOST Search related meetinas v3Post coarch deals> Ticketsv Uicofull> PoST filter per comoanv / onlv open deal stagesGet engagements old associated by dealments old associated by companvSustem Resource WarningSustem resources are constrained. Thesystem mav not be able to generate the loadeded for this test and the cest is likely toa Connect GitConcole 5.) TerminaPOST search de • POST Reada bal • POST Read a bat eGET ReadGET ReadGET read callGET Get EngageGET Read CopyGET httos:/lapi.rm lteration run HSPOST search contaPOSt search conta• Iteration run SD Iteration run SelNo environmentH7).u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileSummaryTotal requests sent ©6.550Requests/second ©Avg. response time ©P90 ©109.04155 ms173 msp95 O196 msP99 ©287 msError % ©0,00Failure % ©Peak CPU % ©Peak Memory % ©0,00100.0 %195 %Filter bv requestsvAva. response% 100262 ms 140 req/s25017•08.25|17-08-2117•08•2717•08•4617•08•4917-08-55)17•09•0117•09•0717.09-1217.09-19- Requests/second - Ava. response - Error % - Virtual users ..• CPU % ••• Memory %Dorformonss dotolle fortotol durotinnPOST search contact by email Copy6.550108.480.00Failure %0.00Resp. time (Ava. ms)Min (ms)Max ms)693287Globals Vault Tools?000...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4577
|
166
|
36
|
2026-05-07T14:10:31.959032+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163031959_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":20,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":22,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":20,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":22,"bounds":{"left":0.22240691,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":22,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). It's a separate bucket from the 190/10s, and critically, search responses do not include the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— you can't read remaining capacity off the response, you have to track it yourself.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search endpoints return","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The daily headers are not returned for OAuth-authenticated calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— that's enforced for marketplace listing, but it's also a sane operational target.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So you have","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"four buckets","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Where to store it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EXPIRE","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"plus Lua scripting for true CAS semantics.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Key layout I'd use:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:search:{portalId} ZSET (sliding 1s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId} HASH (last seen headers)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The two short-window buckets are sliding-window-log (Redis ZSET, score =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"microtime","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The daily bucket is a plain","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with TTL set to seconds-until-midnight in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubSpot's account timezone","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to read it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two-phase: optimistic pre-check, authoritative post-update.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Before a call, run the Lua acquire script. If it returns \"denied,\" sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"After the call, parse","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and store them in","depth":26,"on_screen":false,"role_description":"text"}]...
|
-3281040587340693975
|
1300589750295025884
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in...
|
4576
|
NULL
|
NULL
|
NULL
|
|
4578
|
165
|
36
|
2026-05-07T14:10:32.141698+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163032141_m1.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":20,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":20,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). It's a separate bucket from the 190/10s, and critically, search responses do not include the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— you can't read remaining capacity off the response, you have to track it yourself.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search endpoints return","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The daily headers are not returned for OAuth-authenticated calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— that's enforced for marketplace listing, but it's also a sane operational target.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So you have","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"four buckets","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Where to store it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EXPIRE","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"plus Lua scripting for true CAS semantics.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Key layout I'd use:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:search:{portalId} ZSET (sliding 1s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId} HASH (last seen headers)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The two short-window buckets are sliding-window-log (Redis ZSET, score =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"microtime","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The daily bucket is a plain","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with TTL set to seconds-until-midnight in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubSpot's account timezone","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to read it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two-phase: optimistic pre-check, authoritative post-update.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Before a call, run the Lua acquire script. If it returns \"denied,\" sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"After the call, parse","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and store them in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For search specifically, since headers don't come back, the local counter","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on 429 (you really did make that request).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with multiple jobs","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"}]...
|
431449263103007848
|
1300660119039465692
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4579
|
165
|
37
|
2026-05-07T14:10:34.039436+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163034039_m1.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":20,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":20,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"}]...
|
-7890704746138911267
|
1300589320792267932
|
click
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search...
|
4578
|
NULL
|
NULL
|
NULL
|
|
4580
|
166
|
37
|
2026-05-07T14:10:34.039423+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163034039_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":20,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":22,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":20,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":22,"bounds":{"left":0.22240691,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":22,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). It's a separate bucket from the 190/10s, and critically, search responses do not include the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— you can't read remaining capacity off the response, you have to track it yourself.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search endpoints return","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The daily headers are not returned for OAuth-authenticated calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— that's enforced for marketplace listing, but it's also a sane operational target.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So you have","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"four buckets","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Where to store it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EXPIRE","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"plus Lua scripting for true CAS semantics.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Key layout I'd use:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":28,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:search:{portalId} ZSET (sliding 1s)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId} HASH (last seen headers)","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The two short-window buckets are sliding-window-log (Redis ZSET, score =","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"microtime","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The daily bucket is a plain","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"INCR","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with TTL set to seconds-until-midnight in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubSpot's account timezone","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to read it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two-phase: optimistic pre-check, authoritative post-update.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Before a call, run the Lua acquire script. If it returns \"denied,\" sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"After the call, parse","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and store them in","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot:rl:meta:{portalId}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For search specifically, since headers don't come back, the local counter","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on 429 (you really did make that request).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with multiple jobs","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Concurrency cap","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"via","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue::throttle()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Per-tenant fairness","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Priority lanes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-priority","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hubspot-bulk","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Backoff on 429","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— respect the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"header. If absent, exponential with jitter (e.g.,","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"min(2^attempt * 250ms, 30s) ± 20%","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"How to work with paginated requests","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"How to work with paginated requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The trap is treating \"fetch all\" as one logical operation. Each page is its own API call and competes for tokens with everything else.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Two practical rules:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Don't hold the worker idle while paginating.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"processing","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"of page N as a separate job, and queue a \"fetch page N+1\" job with a delay equal to the wait time. Each page becomes its own atomic unit.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Always batch where HubSpot offers it.","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/{type}/batch/{read|update|create}","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"accepts up to 100 IDs per call. For your \"patching IDs\" flow this is the difference between 600 calls and 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Walkthrough: 600 opportunities","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Walkthrough: 600 opportunities","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sane flow:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Identify deltas","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— if you can avoid Search at all, do. Use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"current state —","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/read","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 IDs per call → 6 calls. Burst weight 6.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Compute diff","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"in your code (no API).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Batch update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v3/objects/deals/batch/update","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"with 100 per call → 6 calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Associations","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST /crm/v4/associations/{from}/{to}/batch/create","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if needed → another few batch calls.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Recognition: pre-check denies if","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"<weight>","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"slots aren't free; on 429 the response surfaces","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(DAILY/SECONDLY) — log it, push the job back with","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"delay, increment a metric so you can alarm on sustained 429 rate.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Pitfalls & bottlenecks worth pre-empting","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Pitfalls & bottlenecks worth pre-empting","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily quota is shared across the whole portal","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search is uniquely fragile","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"hs_object_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ranges or by","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"lastmodifieddate","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"windows.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry storms after HubSpot incidents","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhook → sync → webhook loops","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"jy_last_sync_id","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":") and ignore webhooks where it just changed.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Token refresh","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"expires_in - 60s","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", single-flight the refresh (only one worker refreshes; others wait on a lock).","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search latency for fresh writes","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Clock skew","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— Redis is authoritative. Don't use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"time()","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from each worker for window scoring; use","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"redis.call('TIME')","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"inside the Lua script.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dead workers with held tokens","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The 5% error rule","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Phantom contention with Laravel","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimited","depth":29,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"middleware","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":": it's per-job-class by default, not per-portal. If your","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncDealJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"and","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateActivityJob","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotRateLimiter","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"service that everything routes through.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"You said: What will happen if there are two different types of rate limits?","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: What will happen if there are two different types of rate limits?","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"What will happen if there are two different types of rate limits?","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search requests that have 5 requests per second","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"13:40","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: They're independent buckets — with one shared bucket on top.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Analyzed independent rate limit buckets and their practical interactions","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
1599834330293286419
|
1877155883379416156
|
click
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it
Redis. It's the only realistic option once you have multiple queue workers — DB-backed counters serialize too much, and in-process state can't coordinate across workers. Laravel already speaks Redis natively, and you get atomic
INCR
/
EXPIRE
plus Lua scripting for true CAS semantics.
Key layout I'd use:
Copy to clipboard
hubspot:rl:burst:{portalId}:{appId} ZSET (sliding 10s)
hubspot:rl:search:{portalId} ZSET (sliding 1s)
hubspot:rl:daily:{portalId} STRING + TTL to midnight TZ
hubspot:rl:meta:{portalId} HASH (last seen headers)
The two short-window buckets are sliding-window-log (Redis ZSET, score =
microtime
, member = unique request id). Lua script removes entries older than the window, counts remaining slots, and only adds the new one if there's room — all atomic. Fixed windows are simpler but allow 2× the limit at the boundary, which on a 5/s window is brutal.
The daily bucket is a plain
INCR
with TTL set to seconds-until-midnight in
HubSpot's account timezone
, not yours — Sofia is UTC+2/+3 but your portal might be set to US Eastern. Get this once and cache it.
How to read it
How to read it
Two-phase: optimistic pre-check, authoritative post-update.
Before a call, run the Lua acquire script. If it returns "denied," sleep until a slot frees up (the script can return ms-until-next-slot) or push the job back to the queue with a delay. Don't busy-loop.
After the call, parse
X-HubSpot-RateLimit-Remaining
/
-Max
/
-Interval-Milliseconds
and store them in
hubspot:rl:meta:{portalId}
. This is your reality check — if your local counter says 50 remaining but HubSpot's header says 5, you trust HubSpot and clamp your counter. This handles clock drift, missed accounting (e.g., crashed worker that took a token but never made the request), and other apps in the same account consuming the daily budget invisibly.
For search specifically, since headers don't come back, the local counter
is
the source of truth — meaning if you ever crash mid-flight, you've under-counted. Always release tokens on connection-level failure but
not
on 429 (you really did make that request).
How to work with multiple jobs
How to work with multiple jobs
The queue layer needs to enforce concurrency separately from the rate limiter. Both work together:
Concurrency cap
via
Queue::throttle()
or a Redis semaphore — limits how many sync workers run in parallel against HubSpot. Without this, you can have 50 workers all blocked waiting for tokens, which is wasteful and creates retry storms.
Per-tenant fairness
— if you sync many Jiminny customers into different HubSpot portals, each portal has its own bucket but you still want one slow portal not to starve others. Either separate queues per portal or a fair-share scheduler.
Priority lanes
— webhook-driven updates (user-visible latency) should outrank background batch syncs. Two queues:
hubspot-priority
and
hubspot-bulk
, with priority workers taking 70% of the burst budget and bulk taking 30% (enforce via separate sub-buckets if you really want hard separation, otherwise just pull from priority first).
Backoff on 429
— respect the
Retry-After
header. If absent, exponential with jitter (e.g.,
min(2^attempt * 250ms, 30s) ± 20%
). The jitter is non-negotiable; without it, simultaneous 429s retry in lockstep and you get thundering-herd 429s on retry.
How to work with paginated requests
How to work with paginated requests
The trap is treating "fetch all" as one logical operation. Each page is its own API call and competes for tokens with everything else.
Two practical rules:
Don't hold the worker idle while paginating.
If page N takes 500ms and page N+1 needs to wait 800ms for a token, you've burned 800ms of worker time doing nothing. Instead, fetch page N, dispatch the
processing
of page N as a separate job, and queue a "fetch page N+1" job with a delay equal to the wait time. Each page becomes its own atomic unit.
Always batch where HubSpot offers it.
POST /crm/v3/objects/{type}/batch/{read|update|create}
accepts up to 100 IDs per call. For your "patching IDs" flow this is the difference between 600 calls and 6.
Walkthrough: 600 opportunities
Walkthrough: 600 opportunities
Naive flow (one PATCH per deal): 600 calls. At 190/10s that's ~32s of API time, well under daily. You'd hit burst easily without throttling. Plus likely 1-3 search calls upfront and N association calls.
Sane flow:
Identify deltas
— if you can avoid Search at all, do. Use
lastmodifieddate
filter on a single search if you must, paginate at limit=200. For 600 deals that's 3 search calls @ 5/s = ~600ms naturally throttled.
Batch read
current state —
POST /crm/v3/objects/deals/batch/read
with 100 IDs per call → 6 calls. Burst weight 6.
Compute diff
in your code (no API).
Batch update
—
POST /crm/v3/objects/deals/batch/update
with 100 per call → 6 calls.
Associations
—
POST /crm/v4/associations/{from}/{to}/batch/create
if needed → another few batch calls.
Total: ~15-20 API calls for 600 deals. Burst budget consumed: 20/190 over ~5s. Daily impact: trivial.
Recognition: pre-check denies if
<weight>
slots aren't free; on 429 the response surfaces
policyName
(DAILY/SECONDLY) — log it, push the job back with
Retry-After
delay, increment a metric so you can alarm on sustained 429 rate.
Pitfalls & bottlenecks worth pre-empting
Pitfalls & bottlenecks worth pre-empting
Daily quota is shared across the whole portal
, including any n8n flows, other private apps, marketing automation calling APIs, etc. Budget for ~70-80% of the documented limit, not 100%. Add an internal per-app daily cap so a runaway sync can't starve other apps.
Search is uniquely fragile
: separate 5/s bucket, no headers to read, 10k-result cap per query. If you ever hit 10k+ deals matching a filter, partition by
hs_object_id
ranges or by
lastmodifieddate
windows.
Retry storms after HubSpot incidents
— always jitter, always cap total retries (e.g., 5 attempts, then dead-letter).
Webhook → sync → webhook loops
— when your update fires a HubSpot webhook that triggers another sync. Tag your own writes (e.g., a custom property
jy_last_sync_id
) and ignore webhooks where it just changed.
Token refresh
— OAuth refresh itself counts against burst. Cache the access token in Redis with TTL =
expires_in - 60s
, single-flight the refresh (only one worker refreshes; others wait on a lock).
Search latency for fresh writes
— newly created/updated objects don't appear in search instantly. Don't write-then-search; use returned IDs directly.
Clock skew
— Redis is authoritative. Don't use
time()
from each worker for window scoring; use
redis.call('TIME')
inside the Lua script.
Dead workers with held tokens
— sliding-window-log handles this naturally because tokens expire by score; token-bucket implementations need explicit release-on-crash logic.
The 5% error rule
counts every 429 toward your error budget, even successfully retried ones. If your pre-check is working well, 429s should be near zero — if they're not, your pre-check is wrong, not just slow.
Phantom contention with Laravel
RateLimited
middleware
: it's per-job-class by default, not per-portal. If your
SyncDealJob
and
UpdateActivityJob
are different classes both calling HubSpot, they don't share a Laravel-level limiter — you need a single
HubspotRateLimiter
service that everything routes through.
If you want, I can sketch the Lua script for the sliding-window acquire and the Laravel service wrapper around it.
Copy
Give positive feedback
Give negative feedback
Retry
You said: What will happen if there are two different types of rate limits?
You said: What will happen if there are two different types of rate limits?
What will happen if there are two different types of rate limits?
Search requests that have 5 requests per second
The other rate limit that has, let's say, 100 requests per 10 seconds How do these work together? Is there some conflict? Can I reach the limit with one request and block either as well, or are these separated? Can I still continue making search requests once I hit the normal requests limits?
13:40
Retry
Edit
Copy
Claude responded: They're independent buckets — with one shared bucket on top.
Claude responded: They're independent buckets — with one shared bucket on top.
Analyzed independent rate limit buckets and their practical interactions...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4581
|
166
|
38
|
2026-05-07T14:10:45.197183+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163045197_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4292681604235724203
|
-3311027427328697316
|
visual_change
|
hybrid
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
ClaudeFileEditVIeWWindowHelpHubSpot rate limit implementation strategythis is the headers I getWrite a message…Opus 4.7 Adaptive ~Hubspot rate limits reference - MDCopyUse timeZone to interpret resetsAt from the daily endpoint.Cheat sheet: profiling a new portal in PostmanThree calls, in order:1. GET /account-into/vs/details → portalinto + read durst neaders trom responseGET /account-info/v3/api-usage/daily/private-apps → daily limits (onlymeaningful for private apps)3. Skip search probing — the 5/sec is fixedError response shape"status""error"."message": "You have reached your secondly limit.","errorType" : "RATE_LIMIT","policyName" : "SECONDLY","...","requestiotnolncvname values.SECONDLY - search bucket (5/sec)• TEN_SECONDLY_ROLLING - burst bucket (110/10s for OAuth, 100-250/10s forprivate)• DAILY — private apps daily ceilingAlways inspect policyName on 429 to know which bucket fired and which queue toback off.Other operational guidelines• Error responses must stay under 5% of total daily requests for marketplacecertificationi• Polling endpoints: minimum interval 5 minutes.• Search query: max 3,000 chars, max 18 filters across 5 filter groups, max 10,000 totalresults per query.• Batch endpoints: up to 100 records per call (read create update).• Searchwho can edit, or upgrade to restore collaboration.GET ReadGET read call • GET Get Engage •$0Lukas/Stefka 121 • in 20mGET Read CopyGET https://api.tIteration run HSPOST search contaPOST search conta© Iteration run Se100% L2Thu 7 May 17:10:45UparadeIteration run SeNo environmentAvg. response time ©155 msP90 ©173 msP9S ©196 msP99 ©287 msError % ©0,00Failure % ©0,00Peak CPU % ©100.0 %Peak Memory % ©195 %Filter bv reauestsvAva. response262 ms 140 req/s25017•08•4617•08•4917-08-55)17•09•016,550108.48Error %0.00Falture %0.00Resp. time (Ava. ms)17:09:0717.09-1217.09-19- Requests/second - Ava. response - Error % - Virtual users ..• CPU % •• Memory %Min (ms)Max (ms)693287Globals Vault Tools 5000...
|
4580
|
NULL
|
NULL
|
NULL
|
|
4582
|
165
|
38
|
2026-05-07T14:10:45.583810+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163045583_m1.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":20,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":20,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"You said: So just a solution for rate limit implementation.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"You said: So just a solution for rate limit implementation.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating activity (engagement, meeting, call, note)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"searching for a company or contact","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updating deal","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"patching IDs\nReview what the limit is at the moment in the HubSpot documentation and then think of the solution:","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"where to store it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to read it","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with multiple jobs","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":26,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). Example of such a error Client error:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"POST https://api.hubapi.com/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resulted in a","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 Too Many Requests","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response: {\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10:42","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: Current HubSpot limits (Apr 2026)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":23,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Current HubSpot limits (Apr 2026)","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Current HubSpot limits (Apr 2026)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The burst window is per app; the daily quota is per portal.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". That's the limit the 429 in your example is hitting (","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"secondly limit","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"on","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/crm/v3/objects/contact/search","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"). It's a separate bucket from the 190/10s, and critically, search responses do not include the","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headers","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— you can't read remaining capacity off the response, you have to track it yourself.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Non-search endpoints return","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", and","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The daily headers are not returned for OAuth-authenticated calls","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"— that's enforced for marketplace listing, but it's also a sane operational target.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXLink","text":"hubspot","depth":27,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"hubspot","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So you have","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"four buckets","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Where to store it","depth":25,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Where to store it","depth":26,"on_screen":false,"role_description":"text"}]...
|
-7456715514527240480
|
1300589458237511900
|
click
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Close
Share chat
Claude finished the response
You said: So just a solution for rate limit implementation.
You said: So just a solution for rate limit implementation.
So just a solution for rate limit implementation. The situation currently is that there are some daily and secondary limits for HubSpot. I would like to make sure I never reach a 429 error rate limit. The requests can come from any flow. It can be:
updating activity (engagement, meeting, call, note)
searching for a company or contact
updating deal
patching IDs
Review what the limit is at the moment in the HubSpot documentation and then think of the solution:
where to store it
how to read it
how to work with multiple jobs
how to work with paginated requests Let's consider a situation when there are 600 opportunities to sync. What would be the flow? How would the rate limit be recognized? How would it be handled? What are the potential issues? [API usage guidelines and limits - HubSpot docs](
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
https://developers.hubspot.com/docs/developer-tooling/platform/usage-guidelines
). Example of such a error Client error:
POST https://api.hubapi.com/crm/v3/objects/contact/search
resulted in a
429 Too Many Requests
response: {"status":"error","message":"You have reached your secondly limit.","errorType":"RATE_LIMIT","correlationId":"019dffc4-4 (truncated...). Think carefully about the implementation and potential issue and bottlenecks.
Show more
10:42
Retry
Edit
Copy
Claude responded: Current HubSpot limits (Apr 2026)
Claude responded: Current HubSpot limits (Apr 2026)
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Synthesized HubSpot rate limit architecture with Redis storage and sliding window algorithms
Current HubSpot limits (Apr 2026)
Current HubSpot limits (Apr 2026)
For privately distributed apps (which is what Jiminny presumably uses with HubSpot), the limits are layered: a per-app burst window of 100/10s on Free/Starter, 190/10s on Pro/Enterprise, or 250/10s with the API Limit Increase add-on
, plus a per-account daily cap of 250k / 625k / 1M depending on tier — shared across all private apps in the account
. The burst window is per app; the daily quota is per portal.
hubspot
hubspot
hubspot
hubspot
The Search API is special: five requests per second per account, capped at 200 records per page, with a hard 10,000-result ceiling per query
. That's the limit the 429 in your example is hitting (
secondly limit
on
/crm/v3/objects/contact/search
). It's a separate bucket from the 190/10s, and critically, search responses do not include the
X-HubSpot-RateLimit-*
headers
— you can't read remaining capacity off the response, you have to track it yourself.
hubspot
hubspot
hubspot
hubspot
Non-search endpoints return
X-HubSpot-RateLimit-Remaining
,
-Max
, and
-Interval-Milliseconds
. The daily headers are not returned for OAuth-authenticated calls
, so for those you check the account-info endpoint or maintain your own counter. There's also a soft rule: errors should stay under 5% of total daily requests
— that's enforced for marketplace listing, but it's also a sane operational target.
hubspot
hubspot
hubspot
hubspot
So you have
four buckets
to track at minimum: app-burst (10s sliding), search-secondly (1s sliding), account-daily (24h fixed, midnight in HubSpot's TZ), and per-app daily share (your own budgeting on top of the account cap).
Where to store it
Where to store it...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4583
|
166
|
39
|
2026-05-07T14:10:48.908507+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163048908_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Claude is responding
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Edit
Copy
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Stop response
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning
Burst ceiling
Calls left in current window
Window size, always
10000
Deprecated, ignore
Deprecated, ignore
Header
Meaning
Inferring app type from Max
Inferring app type from
Max
Max
value
App type
100
Free/Starter private app
110
Public OAuth app (marketplace)
190
Pro/Enterprise private app
250
Private app + API Limit Increase add-on
Max
value
100
110
190
250
App type
Free/Starter private app
Public OAuth app (marketplace)
Pro/Enterprise private app
Private app + API Limit Increase add-on
Max
value
App type
The API Limit Increase add-on does
not
raise the limit for public OAuth apps.
2. Search limit
2. Search limit
Cannot be queried.
No endpoint, no headers (search responses strip them).
Fixed constant:
5 requests per second per portal
. The 10,000-result ceiling per query and 200-records-per-page also apply.
Only confirmable by deliberately triggering a 429:
Copy to clipboard...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is responding","depth":21,"bounds":{"left":0.008976064,"top":0.06384677,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.008976064,"top":0.065442935,"width":0.0039893617,"height":0.015961692}},{"char_start":1,"char_count":19,"bounds":{"left":0.012965426,"top":0.065442935,"width":0.048537236,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"bounds":{"left":0.057845745,"top":0.110135674,"width":0.16788563,"height":0.14445332},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.05817819,"top":0.110135674,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1421,"bounds":{"left":0.05817819,"top":0.110135674,"width":0.16755319,"height":0.52274543}}],"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"bounds":{"left":0.05319149,"top":0.25538707,"width":0.13397606,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"bounds":{"left":0.2044548,"top":0.29928172,"width":0.008643617,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.20478724,"top":0.29928172,"width":0.0016622341,"height":0.011971269}},{"char_start":1,"char_count":4,"bounds":{"left":0.20611702,"top":0.29928172,"width":0.00731383,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Edit","depth":22,"bounds":{"left":0.21575798,"top":0.29209897,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"bounds":{"left":0.22639628,"top":0.29209897,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Write a message…","depth":25,"bounds":{"left":0.016289894,"top":0.90901834,"width":0.22174202,"height":0.018355945},"on_screen":true,"value":"Write a message…","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Write a message…","depth":28,"bounds":{"left":0.016289894,"top":0.90981644,"width":0.04654255,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.016289894,"top":0.91061455,"width":0.005319149,"height":0.015961692}},{"char_start":1,"char_count":15,"bounds":{"left":0.021609042,"top":0.91061455,"width":0.041223403,"height":0.015961692}}],"role_description":"text"},{"role":"AXPopUpButton","text":"Add files, connectors, and more","depth":27,"bounds":{"left":0.014960106,"top":0.93695134,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Model: Opus 4.7 Adaptive","depth":27,"bounds":{"left":0.17220744,"top":0.93695134,"width":0.052526597,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Opus 4.7","depth":29,"bounds":{"left":0.17553191,"top":0.9425379,"width":0.019281914,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.17586437,"top":0.9425379,"width":0.0039893617,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.17952128,"top":0.9425379,"width":0.015625,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Adaptive","depth":28,"bounds":{"left":0.19680852,"top":0.9425379,"width":0.019946808,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.19680852,"top":0.9425379,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.20013298,"top":0.9425379,"width":0.01662234,"height":0.014365523}}],"role_description":"text"},{"role":"AXButton","text":"Stop response","depth":25,"bounds":{"left":0.22739361,"top":0.93695134,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":22,"bounds":{"left":0.061502658,"top":0.980846,"width":0.12832446,"height":0.011971269},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":23,"bounds":{"left":0.061502658,"top":0.980846,"width":0.12832446,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.061835106,"top":0.98164403,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":65,"bounds":{"left":0.06482713,"top":0.98164403,"width":0.12533244,"height":0.011971269}}],"role_description":"text"},{"role":"AXRadioButton","text":"Preview","depth":20,"bounds":{"left":0.25831118,"top":0.035913806,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Code","depth":20,"bounds":{"left":0.26761967,"top":0.035913806,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Hubspot rate limits reference · MD","depth":19,"bounds":{"left":0.28025267,"top":0.039106146,"width":0.14960106,"height":0.015961692},"on_screen":true,"help_text":"Hubspot rate limits reference","role_description":"heading"},{"role":"AXStaticText","text":"Hubspot rate limits reference","depth":20,"bounds":{"left":0.28025267,"top":0.03990423,"width":0.06416223,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.28025267,"top":0.03990423,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":28,"bounds":{"left":0.2839096,"top":0.03990423,"width":0.06050532,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"·","depth":20,"bounds":{"left":0.3444149,"top":0.03990423,"width":0.0033244682,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MD","depth":20,"bounds":{"left":0.34740692,"top":0.03990423,"width":0.007978723,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":19,"bounds":{"left":0.44414893,"top":0.03431764,"width":0.015625,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go back","depth":19,"bounds":{"left":0.48271278,"top":0.032721467,"width":0.011968086,"height":0.028731046},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"HubSpot Rate Limit Reference","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"HubSpot Rate Limit Reference","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Quick reference for checking what rate limits apply to a portal.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Three buckets, three discovery methods","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Three buckets, three discovery methods","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"1. Burst limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"1. Burst limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Make any non-search call and read the","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response headers","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(not body).","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"http","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GET https://api.hubapi.com/account-info/v3/details","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Authorization","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bearer {portal_access_token}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Headers in response:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst ceiling","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calls left in current window","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Window size, always","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10000","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst ceiling","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calls left in current window","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Window size, always","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10000","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Inferring app type from Max","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Inferring app type from","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"100","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free/Starter private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"110","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Public OAuth app (marketplace)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"190","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro/Enterprise private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private app + API Limit Increase add-on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"100","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"110","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"190","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free/Starter private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Public OAuth app (marketplace)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro/Enterprise private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private app + API Limit Increase add-on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The API Limit Increase add-on does","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"raise the limit for public OAuth apps.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"2. Search limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"2. Search limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cannot be queried.","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No endpoint, no headers (search responses strip them).","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed constant:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"5 requests per second per portal","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The 10,000-result ceiling per query and 200-records-per-page also apply.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only confirmable by deliberately triggering a 429:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3497990220503316112
|
-5598019310179103524
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Claude is responding
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Edit
Copy
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Stop response
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning
Burst ceiling
Calls left in current window
Window size, always
10000
Deprecated, ignore
Deprecated, ignore
Header
Meaning
Inferring app type from Max
Inferring app type from
Max
Max
value
App type
100
Free/Starter private app
110
Public OAuth app (marketplace)
190
Pro/Enterprise private app
250
Private app + API Limit Increase add-on
Max
value
100
110
190
250
App type
Free/Starter private app
Public OAuth app (marketplace)
Pro/Enterprise private app
Private app + API Limit Increase add-on
Max
value
App type
The API Limit Increase add-on does
not
raise the limit for public OAuth apps.
2. Search limit
2. Search limit
Cannot be queried.
No endpoint, no headers (search responses strip them).
Fixed constant:
5 requests per second per portal
. The 10,000-result ceiling per query and 200-records-per-page also apply.
Only confirmable by deliberately triggering a 429:
Copy to clipboard...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4584
|
165
|
39
|
2026-05-07T14:10:49.540660+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163049540_m1.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Claude is responding
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Edit
Copy
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Stop response
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning
Burst ceiling
Calls left in current window
Window size, always
10000
Deprecated, ignore
Deprecated, ignore
Header
Meaning
Inferring app type from Max
Inferring app type from
Max
Max
value
App type
100
Free/Starter private app
110
Public OAuth app (marketplace)
190
Pro/Enterprise private app
250
Private app + API Limit Increase add-on
Max
value
100
110
190
250
App type
Free/Starter private app
Public OAuth app (marketplace)
Pro/Enterprise private app
Private app + API Limit Increase add-on
Max
value...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is responding","depth":21,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Edit","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Write a message…","depth":25,"on_screen":true,"value":"Write a message…","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Write a message…","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add files, connectors, and more","depth":27,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Model: Opus 4.7 Adaptive","depth":27,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Opus 4.7","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Adaptive","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Stop response","depth":25,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":22,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Preview","depth":20,"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Code","depth":20,"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Hubspot rate limits reference · MD","depth":19,"on_screen":true,"help_text":"Hubspot rate limits reference","role_description":"heading"},{"role":"AXStaticText","text":"Hubspot rate limits reference","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"·","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MD","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go back","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"HubSpot Rate Limit Reference","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"HubSpot Rate Limit Reference","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Quick reference for checking what rate limits apply to a portal.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Three buckets, three discovery methods","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Three buckets, three discovery methods","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"1. Burst limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"1. Burst limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Make any non-search call and read the","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response headers","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(not body).","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"http","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GET https://api.hubapi.com/account-info/v3/details","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Authorization","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bearer {portal_access_token}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Headers in response:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst ceiling","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calls left in current window","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Window size, always","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10000","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst ceiling","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calls left in current window","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Window size, always","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10000","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Inferring app type from Max","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Inferring app type from","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"100","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free/Starter private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"110","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Public OAuth app (marketplace)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"190","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro/Enterprise private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private app + API Limit Increase add-on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"100","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"110","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"190","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free/Starter private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Public OAuth app (marketplace)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro/Enterprise private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private app + API Limit Increase add-on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"}]...
|
-6886888080881466047
|
-7903862319392799526
|
click
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Claude is responding
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Edit
Copy
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Stop response
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning
Burst ceiling
Calls left in current window
Window size, always
10000
Deprecated, ignore
Deprecated, ignore
Header
Meaning
Inferring app type from Max
Inferring app type from
Max
Max
value
App type
100
Free/Starter private app
110
Public OAuth app (marketplace)
190
Pro/Enterprise private app
250
Private app + API Limit Increase add-on
Max
value
100
110
190
250
App type
Free/Starter private app
Public OAuth app (marketplace)
Pro/Enterprise private app
Private app + API Limit Increase add-on
Max
value...
|
4582
|
NULL
|
NULL
|
NULL
|
|
4585
|
166
|
40
|
2026-05-07T14:11:20.348672+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163080348_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Claude is responding
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Edit
Copy
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
**P
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Stop response
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning
Burst ceiling
Calls left in current window
Window size, always
10000
Deprecated, ignore
Deprecated, ignore
Header
Meaning
Inferring app type from Max
Inferring app type from
Max
Max
value
App type
100
Free/Starter private app
110
Public OAuth app (marketplace)
190
Pro/Enterprise private app
250
Private app + API Limit Increase add-on
Max
value
100
110
190
250
App type
Free/Starter private app
Public OAuth app (marketplace)
Pro/Enterprise private app
Private app + API Limit Increase add-on
Max
value
App type
The API Limit Increase add-on does
not
raise the limit for public OAuth apps.
2. Search limit
2. Search limit
Cannot be queried.
No endpoint, no headers (search responses strip them).
Fixed constant:
5 requests per second per portal
. The 10,000-result ceiling per query and 200-records-per-page also apply.
Only confirmable by deliberately triggering a 429:
Copy to clipboard
json
{
"status"
:
"error"
,
"message"
:
"You have reached your secondly limit."
,
"errorType"
:
"RATE_LIMIT"
,
"policyName"
:
"SECONDLY"
}
policyName: "SECONDLY"
from a search endpoint = search bucket. Track this server-side; HubSpot won't help.
3. Daily limit
3. Daily limit
Public OAuth apps: no daily limit
Public OAuth apps:
no daily limit
Skip the daily bucket entirely. OAuth apps are constrained by burst (110/10s) only. Theoretical max sustained = 950,400 calls/day per portal.
Private apps: query the endpoint
Private apps: query the endpoint
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps
Authorization
:
Bearer {portal_access_token}
Returns:
Copy to clipboard
json
{
"results"
:
[
{
"name"
:
"private-apps-api-calls-daily"
,
"usageLimit"
:
1000000
,
"currentUsage"
:
47213
,
"collectedAt"
:
"2026-05-07T11:23:01.362Z"
,
"fetchStatus"
:
"SUCCESS"
,
"resetsAt"...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is responding","depth":21,"bounds":{"left":0.008976064,"top":0.06384677,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.008976064,"top":0.065442935,"width":0.0039893617,"height":0.015961692}},{"char_start":1,"char_count":19,"bounds":{"left":0.012965426,"top":0.065442935,"width":0.048537236,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"bounds":{"left":0.057845745,"top":0.110135674,"width":0.16788563,"height":0.14445332},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.05817819,"top":0.110135674,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1421,"bounds":{"left":0.05817819,"top":0.110135674,"width":0.16755319,"height":0.52274543}}],"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"bounds":{"left":0.05319149,"top":0.25538707,"width":0.13397606,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"bounds":{"left":0.2044548,"top":0.29928172,"width":0.008643617,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.20478724,"top":0.29928172,"width":0.0016622341,"height":0.011971269}},{"char_start":1,"char_count":4,"bounds":{"left":0.20611702,"top":0.29928172,"width":0.00731383,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Edit","depth":22,"bounds":{"left":0.21575798,"top":0.29209897,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"bounds":{"left":0.22639628,"top":0.29209897,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":22,"bounds":{"left":0.017287234,"top":0.3256185,"width":0.21974733,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":23,"bounds":{"left":0.016954787,"top":0.3471668,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.016954787,"top":0.34956107,"width":0.0039893617,"height":0.016759777}},{"char_start":1,"char_count":66,"bounds":{"left":0.020944148,"top":0.34956107,"width":0.16456117,"height":0.016759777}}],"role_description":"text"},{"role":"AXStaticText","text":"That confirms the diagnosis. Look at what's there and — more importantly — what isn't:","depth":25,"bounds":{"left":0.017287234,"top":0.35434955,"width":0.20079787,"height":0.035115723},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.35434955,"width":0.003656915,"height":0.016759777}},{"char_start":1,"char_count":85,"bounds":{"left":0.017287234,"top":0.35434955,"width":0.20113032,"height":0.035913806}}],"role_description":"text"},{"role":"AXStaticText","text":"Missing from the 429 response:","depth":26,"bounds":{"left":0.017287234,"top":0.40223464,"width":0.07945479,"height":0.015961692},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.40223464,"width":0.005319149,"height":0.016759777}},{"char_start":1,"char_count":29,"bounds":{"left":0.022606382,"top":0.40223464,"width":0.07413564,"height":0.016759777}}],"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.43176377,"width":0.0063164895,"height":0.015961692},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"bounds":{"left":0.03557181,"top":0.43256184,"width":0.031914894,"height":0.015163607},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.43335995,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":10,"bounds":{"left":0.03856383,"top":0.43335995,"width":0.028922873,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.4557063,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":27,"bounds":{"left":0.03557181,"top":0.45730248,"width":0.06615692,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.45730248,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":22,"bounds":{"left":0.03856383,"top":0.45730248,"width":0.06349734,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.48044693,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"bounds":{"left":0.03557181,"top":0.4820431,"width":0.08344415,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.4820431,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":28,"bounds":{"left":0.03856383,"top":0.4820431,"width":0.080784574,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.5051876,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":27,"bounds":{"left":0.03557181,"top":0.5067837,"width":0.11801862,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.5067837,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":40,"bounds":{"left":0.03856383,"top":0.5067837,"width":0.11502659,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"**P","depth":25,"bounds":{"left":0.017287234,"top":0.5450918,"width":0.00831117,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.54588985,"width":0.0026595744,"height":0.015961692}},{"char_start":1,"char_count":2,"bounds":{"left":0.019946808,"top":0.54588985,"width":0.005984043,"height":0.015961692}}],"role_description":"text"},{"role":"AXTextArea","text":"Write a message…","depth":25,"bounds":{"left":0.016289894,"top":0.90901834,"width":0.22174202,"height":0.018355945},"on_screen":true,"value":"Write a message…","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Write a message…","depth":28,"bounds":{"left":0.016289894,"top":0.90981644,"width":0.04654255,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.016289894,"top":0.91061455,"width":0.005319149,"height":0.015961692}},{"char_start":1,"char_count":15,"bounds":{"left":0.021609042,"top":0.91061455,"width":0.041223403,"height":0.015961692}}],"role_description":"text"},{"role":"AXPopUpButton","text":"Add files, connectors, and more","depth":27,"bounds":{"left":0.014960106,"top":0.93695134,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Model: Opus 4.7 Adaptive","depth":27,"bounds":{"left":0.17220744,"top":0.93695134,"width":0.052526597,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Opus 4.7","depth":29,"bounds":{"left":0.17553191,"top":0.9425379,"width":0.019281914,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.17586437,"top":0.9425379,"width":0.0039893617,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.17952128,"top":0.9425379,"width":0.015625,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"Adaptive","depth":28,"bounds":{"left":0.19680852,"top":0.9425379,"width":0.019946808,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.19680852,"top":0.9425379,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.20013298,"top":0.9425379,"width":0.01662234,"height":0.014365523}}],"role_description":"text"},{"role":"AXButton","text":"Stop response","depth":25,"bounds":{"left":0.22739361,"top":0.93695134,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":22,"bounds":{"left":0.061502658,"top":0.980846,"width":0.12832446,"height":0.011971269},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":23,"bounds":{"left":0.061502658,"top":0.980846,"width":0.12832446,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.061835106,"top":0.98164403,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":65,"bounds":{"left":0.06482713,"top":0.98164403,"width":0.12533244,"height":0.011971269}}],"role_description":"text"},{"role":"AXRadioButton","text":"Preview","depth":20,"bounds":{"left":0.25831118,"top":0.035913806,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Code","depth":20,"bounds":{"left":0.26761967,"top":0.035913806,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Hubspot rate limits reference · MD","depth":19,"bounds":{"left":0.28025267,"top":0.039106146,"width":0.14960106,"height":0.015961692},"on_screen":true,"help_text":"Hubspot rate limits reference","role_description":"heading"},{"role":"AXStaticText","text":"Hubspot rate limits reference","depth":20,"bounds":{"left":0.28025267,"top":0.03990423,"width":0.06416223,"height":0.013567438},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.28025267,"top":0.03990423,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":28,"bounds":{"left":0.2839096,"top":0.03990423,"width":0.06050532,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"·","depth":20,"bounds":{"left":0.3444149,"top":0.03990423,"width":0.0033244682,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MD","depth":20,"bounds":{"left":0.34740692,"top":0.03990423,"width":0.007978723,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":19,"bounds":{"left":0.44414893,"top":0.03431764,"width":0.015625,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go back","depth":19,"bounds":{"left":0.48271278,"top":0.032721467,"width":0.011968086,"height":0.028731046},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"HubSpot Rate Limit Reference","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"HubSpot Rate Limit Reference","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Quick reference for checking what rate limits apply to a portal.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Three buckets, three discovery methods","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Three buckets, three discovery methods","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"1. Burst limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"1. Burst limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Make any non-search call and read the","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response headers","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(not body).","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"http","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GET https://api.hubapi.com/account-info/v3/details","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Authorization","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bearer {portal_access_token}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Headers in response:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst ceiling","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calls left in current window","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Window size, always","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10000","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst ceiling","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calls left in current window","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Window size, always","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10000","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Inferring app type from Max","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Inferring app type from","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"100","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free/Starter private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"110","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Public OAuth app (marketplace)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"190","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro/Enterprise private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private app + API Limit Increase add-on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"100","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"110","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"190","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free/Starter private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Public OAuth app (marketplace)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro/Enterprise private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private app + API Limit Increase add-on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The API Limit Increase add-on does","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"raise the limit for public OAuth apps.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"2. Search limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"2. Search limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cannot be queried.","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No endpoint, no headers (search responses strip them).","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed constant:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"5 requests per second per portal","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The 10,000-result ceiling per query and 200-records-per-page also apply.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only confirmable by deliberately triggering a 429:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"json","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"status\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"error\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"message\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"You have reached your secondly limit.\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"errorType\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"RATE_LIMIT\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"policyName\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"SECONDLY\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName: \"SECONDLY\"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from a search endpoint = search bucket. Track this server-side; HubSpot won't help.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"3. Daily limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"3. Daily limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Public OAuth apps: no daily limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Public OAuth apps:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"no daily limit","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Skip the daily bucket entirely. OAuth apps are constrained by burst (110/10s) only. Theoretical max sustained = 950,400 calls/day per portal.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Private apps: query the endpoint","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Private apps: query the endpoint","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"http","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GET https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Authorization","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bearer {portal_access_token}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Returns:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"json","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"results\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"name\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"private-apps-api-calls-daily\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"usageLimit\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1000000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"currentUsage\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"47213","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"collectedAt\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"2026-05-07T11:23:01.362Z\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"fetchStatus\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"SUCCESS\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"resetsAt\"","depth":25,"on_screen":false,"role_description":"text"}]...
|
6054281320766257586
|
-7903853523333331750
|
idle
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Claude is responding
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Edit
Copy
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
**P
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Stop response
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning
Burst ceiling
Calls left in current window
Window size, always
10000
Deprecated, ignore
Deprecated, ignore
Header
Meaning
Inferring app type from Max
Inferring app type from
Max
Max
value
App type
100
Free/Starter private app
110
Public OAuth app (marketplace)
190
Pro/Enterprise private app
250
Private app + API Limit Increase add-on
Max
value
100
110
190
250
App type
Free/Starter private app
Public OAuth app (marketplace)
Pro/Enterprise private app
Private app + API Limit Increase add-on
Max
value
App type
The API Limit Increase add-on does
not
raise the limit for public OAuth apps.
2. Search limit
2. Search limit
Cannot be queried.
No endpoint, no headers (search responses strip them).
Fixed constant:
5 requests per second per portal
. The 10,000-result ceiling per query and 200-records-per-page also apply.
Only confirmable by deliberately triggering a 429:
Copy to clipboard
json
{
"status"
:
"error"
,
"message"
:
"You have reached your secondly limit."
,
"errorType"
:
"RATE_LIMIT"
,
"policyName"
:
"SECONDLY"
}
policyName: "SECONDLY"
from a search endpoint = search bucket. Track this server-side; HubSpot won't help.
3. Daily limit
3. Daily limit
Public OAuth apps: no daily limit
Public OAuth apps:
no daily limit
Skip the daily bucket entirely. OAuth apps are constrained by burst (110/10s) only. Theoretical max sustained = 950,400 calls/day per portal.
Private apps: query the endpoint
Private apps: query the endpoint
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps
Authorization
:
Bearer {portal_access_token}
Returns:
Copy to clipboard
json
{
"results"
:
[
{
"name"
:
"private-apps-api-calls-daily"
,
"usageLimit"
:
1000000
,
"currentUsage"
:
47213
,
"collectedAt"
:
"2026-05-07T11:23:01.362Z"
,
"fetchStatus"
:
"SUCCESS"
,
"resetsAt"...
|
4583
|
NULL
|
NULL
|
NULL
|
|
4586
|
165
|
40
|
2026-05-07T14:11:20.701208+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163080701_m1.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Claude is responding
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Edit
Copy
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budg
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Stop response
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning
Burst ceiling
Calls left in current window
Window size, always
10000
Deprecated, ignore
Deprecated, ignore
Header
Meaning
Inferring app type from Max
Inferring app type from
Max
Max
value
App type
100
Free/Starter private app
110
Public OAuth app (marketplace)
190
Pro/Enterprise private app
250
Private app + API Limit Increase add-on
Max
value
100
110
190
250
App type
Free/Starter private app
Public OAuth app (marketplace)
Pro/Enterprise private app
Private app + API Limit Increase add-on
Max
value
App type
The API Limit Increase add-on does
not
raise the limit for public OAuth apps.
2. Search limit
2. Search limit
Cannot be queried.
No endpoint, no headers (search responses strip them).
Fixed constant:
5 requests per second per portal
. The 10,000-result ceiling per query and 200-records-per-page also apply.
Only confirmable by deliberately triggering a 429:
Copy to clipboard
json
{
"status"
:
"error"
,
"message"
:
"You have reached your secondly limit."
,
"errorType"
:
"RATE_LIMIT"
,
"policyName"
:
"SECONDLY"
}
policyName: "SECONDLY"
from a search endpoint = search bucket. Track this server-side; HubSpot won't help.
3. Daily limit
3. Daily limit
Public OAuth apps: no daily limit
Public OAuth apps:
no daily limit
Skip the daily bucket entirely. OAuth apps are constrained by burst (110/10s) only. Theoretical max sustained = 950,400 calls/day per portal.
Private apps: query the endpoint
Private apps: query the endpoint
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps
Authorization
:
Bearer {portal_access_token}
Returns:
Copy to clipboard
json
{
"results"
:
[
{
"name"
:
"private-apps-api-calls-daily"
,
"usageLimit"
:
1000000
,
"currentUsage"
:
47213
,
"collectedAt"
:
"2026-05-07T11:23:01.362Z"
,
"fetchStatus"
:
"SUCCESS"
,
"resetsAt"
:
"2026-05-08T04:00:00Z"
}
]
}
Field
Meaning
usageLimit
Daily ceiling (shared across ALL private apps in portal)
currentUsage
Today's consumption (private apps only — does not count OAuth calls)
resetsAt
Reset time in UTC, based on portal's configured timezone
Field
usageLimit
currentUsage
resetsAt
Meaning
Daily ceiling (shared across ALL private apps in portal)
Today's consumption (private apps only — does not count OAuth calls)
Reset time in UTC, based on portal's configured timezone
Field
Meaning
Daily limit by tier (private apps)
Daily limit by tier (private apps)
Tier
Daily limit
Free / Starter
250,000
Professional
625,000
Enterprise
1,000,000
Any tier + 1× API Limit Increase
+1,000,000
Any tier + 2× API Limit Increase
+2,000,000 (max)
Tier
Free / Starter
Professional
Enterprise
Any tier + 1× API Limit Increase
Any tier + 2× API Limit Increase
Daily limit
250,000
625,000
1,000,000
+1,000,000
+2,000,000 (max)
Tier
Daily limit
Portal context (timezone, tier)
Portal context (timezone, tier)
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Returns:
Copy to clipboard
json
{
"portalId"
:
123456
,
"accountType"
:
"STANDARD"
,
"timeZone"
:
"Europe/Sofia"
,
"utcOffset"
:
"+03:00"
,
"uiDomain"
:
"app.hubspot.com"
,
"dataHostingLocation"
:
"eu1"
}
Use
timeZone
to interpret
resetsAt
from the daily endpoint.
Cheat sheet: profiling a new portal in Postman
Cheat sheet: profiling a new portal in Postman
Three calls, in order:
GET /account-info/v3/details
→ portal info + read burst headers from response
GET /account-info/v3/api-usage/daily/private-apps
→ daily limits (only meaningful for private apps)
Skip search probing — the 5/sec is fixed
Error response shape
Error response shape
Copy to clipboard
json
{
"status"
:
"error"
,
"message"
:
"You have reached your secondly limit."
,
"errorType"
:
"RATE_LIMIT"
,...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is responding","depth":21,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Edit","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"That confirms the diagnosis. Look at what's there and — more importantly — what isn't:","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Missing from the 429 response:","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Present that's actually useful:","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"✅","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(for debugging / filing tickets with HubSpot)","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budg","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Write a message…","depth":25,"on_screen":true,"value":"Write a message…","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Write a message…","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add files, connectors, and more","depth":27,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Model: Opus 4.7 Adaptive","depth":27,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Opus 4.7","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Adaptive","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Stop response","depth":25,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.035555556},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":22,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Preview","depth":20,"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Code","depth":20,"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Hubspot rate limits reference · MD","depth":19,"on_screen":true,"help_text":"Hubspot rate limits reference","role_description":"heading"},{"role":"AXStaticText","text":"Hubspot rate limits reference","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"·","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MD","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go back","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"HubSpot Rate Limit Reference","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"HubSpot Rate Limit Reference","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Quick reference for checking what rate limits apply to a portal.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Three buckets, three discovery methods","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Three buckets, three discovery methods","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"1. Burst limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"1. Burst limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Make any non-search call and read the","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response headers","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(not body).","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"http","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GET https://api.hubapi.com/account-info/v3/details","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Authorization","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bearer {portal_access_token}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Headers in response:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst ceiling","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calls left in current window","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Window size, always","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10000","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst ceiling","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calls left in current window","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Window size, always","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10000","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Inferring app type from Max","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Inferring app type from","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"100","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free/Starter private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"110","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Public OAuth app (marketplace)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"190","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro/Enterprise private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private app + API Limit Increase add-on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"100","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"110","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"190","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free/Starter private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Public OAuth app (marketplace)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Pro/Enterprise private app","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private app + API Limit Increase add-on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"value","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"App type","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The API Limit Increase add-on does","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"not","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"raise the limit for public OAuth apps.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"2. Search limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"2. Search limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cannot be queried.","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"No endpoint, no headers (search responses strip them).","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed constant:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"5 requests per second per portal","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". The 10,000-result ceiling per query and 200-records-per-page also apply.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Only confirmable by deliberately triggering a 429:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"json","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"status\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"error\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"message\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"You have reached your secondly limit.\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"errorType\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"RATE_LIMIT\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"policyName\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"SECONDLY\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"policyName: \"SECONDLY\"","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from a search endpoint = search bucket. Track this server-side; HubSpot won't help.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"3. Daily limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"3. Daily limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Public OAuth apps: no daily limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Public OAuth apps:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"no daily limit","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Skip the daily bucket entirely. OAuth apps are constrained by burst (110/10s) only. Theoretical max sustained = 950,400 calls/day per portal.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Private apps: query the endpoint","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Private apps: query the endpoint","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"http","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GET https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Authorization","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bearer {portal_access_token}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Returns:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"json","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"results\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"name\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"private-apps-api-calls-daily\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"usageLimit\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1000000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"currentUsage\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"47213","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"collectedAt\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"2026-05-07T11:23:01.362Z\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"fetchStatus\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"SUCCESS\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"resetsAt\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"2026-05-08T04:00:00Z\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Field","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"usageLimit","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily ceiling (shared across ALL private apps in portal)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"currentUsage","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Today's consumption (private apps only — does not count OAuth calls)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resetsAt","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Reset time in UTC, based on portal's configured timezone","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Field","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"usageLimit","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"currentUsage","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"resetsAt","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily ceiling (shared across ALL private apps in portal)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Today's consumption (private apps only — does not count OAuth calls)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Reset time in UTC, based on portal's configured timezone","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Field","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Daily limit by tier (private apps)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Daily limit by tier (private apps)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Tier","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free / Starter","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250,000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Professional","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"625,000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Enterprise","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1,000,000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Any tier + 1× API Limit Increase","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+1,000,000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Any tier + 2× API Limit Increase","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+2,000,000 (max)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Tier","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Free / Starter","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Professional","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Enterprise","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Any tier + 1× API Limit Increase","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Any tier + 2× API Limit Increase","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"250,000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"625,000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1,000,000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+1,000,000","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+2,000,000 (max)","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Tier","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Portal context (timezone, tier)","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Portal context (timezone, tier)","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"http","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GET https://api.hubapi.com/account-info/v3/details","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Authorization","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bearer {portal_access_token}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Returns:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"json","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"portalId\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"123456","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"accountType\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"STANDARD\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"timeZone\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"Europe/Sofia\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"utcOffset\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"+03:00\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"uiDomain\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"app.hubspot.com\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"dataHostingLocation\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"\"eu1\"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Use","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"timeZone","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"to interpret","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"resetsAt","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"from the daily endpoint.","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Cheat sheet: profiling a new portal in Postman","depth":22,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Cheat sheet: profiling a new portal in Postman","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Three calls, in order:","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"GET /account-info/v3/details","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ portal info + read burst headers from response","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"GET /account-info/v3/api-usage/daily/private-apps","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ daily limits (only meaningful for private apps)","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Skip search probing — the 5/sec is fixed","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Error response shape","depth":22,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Error response shape","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"json","depth":24,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"\"status\"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"\"error\"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"\"message\"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"\"You have reached your secondly limit.\"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"\"errorType\"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"\"RATE_LIMIT\"","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"}]...
|
3669546698656974618
|
-7903852974651259686
|
idle
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Claude is responding
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Edit
Copy
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budg
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Stop response
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning
Burst ceiling
Calls left in current window
Window size, always
10000
Deprecated, ignore
Deprecated, ignore
Header
Meaning
Inferring app type from Max
Inferring app type from
Max
Max
value
App type
100
Free/Starter private app
110
Public OAuth app (marketplace)
190
Pro/Enterprise private app
250
Private app + API Limit Increase add-on
Max
value
100
110
190
250
App type
Free/Starter private app
Public OAuth app (marketplace)
Pro/Enterprise private app
Private app + API Limit Increase add-on
Max
value
App type
The API Limit Increase add-on does
not
raise the limit for public OAuth apps.
2. Search limit
2. Search limit
Cannot be queried.
No endpoint, no headers (search responses strip them).
Fixed constant:
5 requests per second per portal
. The 10,000-result ceiling per query and 200-records-per-page also apply.
Only confirmable by deliberately triggering a 429:
Copy to clipboard
json
{
"status"
:
"error"
,
"message"
:
"You have reached your secondly limit."
,
"errorType"
:
"RATE_LIMIT"
,
"policyName"
:
"SECONDLY"
}
policyName: "SECONDLY"
from a search endpoint = search bucket. Track this server-side; HubSpot won't help.
3. Daily limit
3. Daily limit
Public OAuth apps: no daily limit
Public OAuth apps:
no daily limit
Skip the daily bucket entirely. OAuth apps are constrained by burst (110/10s) only. Theoretical max sustained = 950,400 calls/day per portal.
Private apps: query the endpoint
Private apps: query the endpoint
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps
Authorization
:
Bearer {portal_access_token}
Returns:
Copy to clipboard
json
{
"results"
:
[
{
"name"
:
"private-apps-api-calls-daily"
,
"usageLimit"
:
1000000
,
"currentUsage"
:
47213
,
"collectedAt"
:
"2026-05-07T11:23:01.362Z"
,
"fetchStatus"
:
"SUCCESS"
,
"resetsAt"
:
"2026-05-08T04:00:00Z"
}
]
}
Field
Meaning
usageLimit
Daily ceiling (shared across ALL private apps in portal)
currentUsage
Today's consumption (private apps only — does not count OAuth calls)
resetsAt
Reset time in UTC, based on portal's configured timezone
Field
usageLimit
currentUsage
resetsAt
Meaning
Daily ceiling (shared across ALL private apps in portal)
Today's consumption (private apps only — does not count OAuth calls)
Reset time in UTC, based on portal's configured timezone
Field
Meaning
Daily limit by tier (private apps)
Daily limit by tier (private apps)
Tier
Daily limit
Free / Starter
250,000
Professional
625,000
Enterprise
1,000,000
Any tier + 1× API Limit Increase
+1,000,000
Any tier + 2× API Limit Increase
+2,000,000 (max)
Tier
Free / Starter
Professional
Enterprise
Any tier + 1× API Limit Increase
Any tier + 2× API Limit Increase
Daily limit
250,000
625,000
1,000,000
+1,000,000
+2,000,000 (max)
Tier
Daily limit
Portal context (timezone, tier)
Portal context (timezone, tier)
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Returns:
Copy to clipboard
json
{
"portalId"
:
123456
,
"accountType"
:
"STANDARD"
,
"timeZone"
:
"Europe/Sofia"
,
"utcOffset"
:
"+03:00"
,
"uiDomain"
:
"app.hubspot.com"
,
"dataHostingLocation"
:
"eu1"
}
Use
timeZone
to interpret
resetsAt
from the daily endpoint.
Cheat sheet: profiling a new portal in Postman
Cheat sheet: profiling a new portal in Postman
Three calls, in order:
GET /account-info/v3/details
→ portal info + read burst headers from response
GET /account-info/v3/api-usage/daily/private-apps
→ daily limits (only meaningful for private apps)
Skip search probing — the 5/sec is fixed
Error response shape
Error response shape
Copy to clipboard
json
{
"status"
:
"error"
,
"message"
:
"You have reached your secondly limit."
,
"errorType"
:
"RATE_LIMIT"
,...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4587
|
166
|
41
|
2026-05-07T14:11:51.150949+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163111150_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''
)
?
:
null
;
}
private
function
withJitter
(
int
$seconds
)
:
int
{
// ±20% randomization to prevent thundering-herd retries
$jitter
=
(
int
)
(
$seconds
*
0.2
)
;
return
max
(
1
,
$seconds
+
random_int
(
-
$jitter
,
$jitter
)
)
;
}
Why those defaults
Why those defaults
SECONDLY
→ 2 seconds.
The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.
TEN_SECONDLY_ROLLING
→ 8 seconds.
The window is 10 seconds
rolling
. If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.
DAILY
→ 3600 (1 hour) or push to next-day queue.
Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)
Jitter
is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.
Bonus: log the correlation ID
Bonus: log the correlation ID
You're throwing away a useful debugging signal. The
x-hubspot-correlation-id
from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support "why did this specific call get rate limited" or correlate with their internal logs if you ever need to escalate. Capture it:
Copy to clipboard
php
$correlationId
=
$this
->
headerValue
(
$headers
,
'x-hubspot-correlation-id'
)
??
(
$payload
[
'correlationId'
]
??
null
)
;
$this
->
log
->
warning
(
'[Hubspot] 429 received'
,
[
'policy'
=>
$policy
,
'retry_after'
=>
$delaySeconds
,
'hubspot_correlation_id'
=>
$correlationId
,
'endpoint'
=>
$endpointForLogging
,
// pass this in
]
)
;
When a customer reports "my sync was slow today" and you can correlate to a flurry of 429s with HubSpot correlation IDs, the diagnosis path is much shorter.
So the final picture
So the final picture
For this codebase, given what HubSpot actually sends:
Pre-acquire from Redis bucket before every call
— this is your primary defense, since post-hoc info is sparse.
On 429, read
policyName
from the body
— the only reliable signal of which bucket fired.
Apply policy-specific backoff with jitter
—
Retry-After
is nice-to-have but rare in practice.
On success, capture
X-HubSpot-RateLimit-*
headers via Guzzle middleware
to clamp the local counter — these
are
present on 200 responses (just confirmed in your earlier Postman screenshot showing
Max=110
).
The asymmetry is the key insight: HubSpot tells you a lot when things are fine, almost nothing when they're not. Your limiter has to be smart enough to work with that.
Copy
Give positive feedback
Give negative feedback
Retry
Scroll to bottom
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Settings
Press and hold to record
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"bounds":{"left":0.008976064,"top":0.06384677,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.008976064,"top":0.065442935,"width":0.0039893617,"height":0.015961692}},{"char_start":1,"char_count":27,"bounds":{"left":0.012965426,"top":0.065442935,"width":0.06948138,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"bounds":{"left":0.057845745,"top":0.110135674,"width":0.16788563,"height":0.14445332},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.05817819,"top":0.110135674,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1421,"bounds":{"left":0.05817819,"top":0.110135674,"width":0.16755319,"height":0.52274543}}],"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"bounds":{"left":0.05319149,"top":0.25538707,"width":0.13397606,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"bounds":{"left":0.16755319,"top":0.29928172,"width":0.008643617,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.16788563,"top":0.29928172,"width":0.0016622341,"height":0.011971269}},{"char_start":1,"char_count":4,"bounds":{"left":0.16921543,"top":0.29928172,"width":0.00731383,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"bounds":{"left":0.17852394,"top":0.29209897,"width":0.010970744,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"bounds":{"left":0.18916224,"top":0.29209897,"width":0.010970744,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"bounds":{"left":0.19980054,"top":0.29209897,"width":0.010970744,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous version","depth":21,"bounds":{"left":0.21043883,"top":0.29209897,"width":0.00831117,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 / 2","depth":21,"bounds":{"left":0.21974733,"top":0.29848364,"width":0.007978723,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.2200798,"top":0.29928172,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":4,"bounds":{"left":0.22240691,"top":0.29928172,"width":0.005319149,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Next version","depth":21,"bounds":{"left":0.22905585,"top":0.29209897,"width":0.007978723,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: That confirms the diagnosis.","depth":21,"bounds":{"left":0.014295213,"top":0.3200319,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: That confirms the diagnosis.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":22,"bounds":{"left":0.017287234,"top":0.3256185,"width":0.21974733,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":23,"bounds":{"left":0.016954787,"top":0.3471668,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.016954787,"top":0.34956107,"width":0.0039893617,"height":0.016759777}},{"char_start":1,"char_count":66,"bounds":{"left":0.020944148,"top":0.34956107,"width":0.16456117,"height":0.016759777}}],"role_description":"text"},{"role":"AXStaticText","text":"That confirms the diagnosis. Look at what's there and — more importantly — what isn't:","depth":25,"bounds":{"left":0.017287234,"top":0.35434955,"width":0.20079787,"height":0.035115723},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.35434955,"width":0.003656915,"height":0.016759777}},{"char_start":1,"char_count":85,"bounds":{"left":0.017287234,"top":0.35434955,"width":0.20113032,"height":0.035913806}}],"role_description":"text"},{"role":"AXStaticText","text":"Missing from the 429 response:","depth":26,"bounds":{"left":0.017287234,"top":0.40223464,"width":0.07945479,"height":0.015961692},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.40223464,"width":0.005319149,"height":0.016759777}},{"char_start":1,"char_count":29,"bounds":{"left":0.022606382,"top":0.40223464,"width":0.07413564,"height":0.016759777}}],"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.43176377,"width":0.0063164895,"height":0.015961692},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"bounds":{"left":0.03557181,"top":0.43256184,"width":0.031914894,"height":0.015163607},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.43335995,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":10,"bounds":{"left":0.03856383,"top":0.43335995,"width":0.028922873,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.4557063,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":27,"bounds":{"left":0.03557181,"top":0.45730248,"width":0.06615692,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.45730248,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":22,"bounds":{"left":0.03856383,"top":0.45730248,"width":0.06349734,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.48044693,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"bounds":{"left":0.03557181,"top":0.4820431,"width":0.08344415,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.4820431,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":28,"bounds":{"left":0.03856383,"top":0.4820431,"width":0.080784574,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.5051876,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":27,"bounds":{"left":0.03557181,"top":0.5067837,"width":0.11801862,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.5067837,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":40,"bounds":{"left":0.03856383,"top":0.5067837,"width":0.11502659,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"Present that's actually useful:","depth":26,"bounds":{"left":0.017287234,"top":0.5450918,"width":0.07480053,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.54588985,"width":0.003656915,"height":0.015961692}},{"char_start":1,"char_count":30,"bounds":{"left":0.020944148,"top":0.54588985,"width":0.07114362,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"✅","depth":26,"bounds":{"left":0.027925532,"top":0.5746209,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":27,"bounds":{"left":0.03557181,"top":0.57621706,"width":0.069148935,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.57621706,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":23,"bounds":{"left":0.03856383,"top":0.57621706,"width":0.06648936,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"(for debugging / filing tickets with HubSpot)","depth":26,"bounds":{"left":0.10605053,"top":0.5746209,"width":0.11037234,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10638298,"top":0.575419,"width":0.0009973404,"height":0.015961692}},{"char_start":1,"char_count":44,"bounds":{"left":0.107380316,"top":0.575419,"width":0.10704787,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the","depth":25,"bounds":{"left":0.017287234,"top":0.61452514,"width":0.20678191,"height":0.055067837},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.61532325,"width":0.0033244682,"height":0.015961692}},{"char_start":1,"char_count":196,"bounds":{"left":0.017287234,"top":0.61532325,"width":0.20678191,"height":0.054269753}}],"role_description":"text"},{"role":"AXStaticText","text":"search","depth":26,"bounds":{"left":0.091755316,"top":0.6528332,"width":0.016954787,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.091755316,"top":0.65363127,"width":0.0029920214,"height":0.015961692}},{"char_start":1,"char_count":5,"bounds":{"left":0.0944149,"top":0.65363127,"width":0.01462766,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"429 (the","depth":25,"bounds":{"left":0.10837766,"top":0.6528332,"width":0.022606382,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1087101,"top":0.65363127,"width":0.0009973404,"height":0.015961692}},{"char_start":1,"char_count":7,"bounds":{"left":0.109707445,"top":0.65363127,"width":0.01761968,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"/contact/search","depth":26,"bounds":{"left":0.13231383,"top":0.6544294,"width":0.043550532,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.13264628,"top":0.6544294,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":14,"bounds":{"left":0.13530585,"top":0.6544294,"width":0.04055851,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"one) — search responses always strip rate-limit headers, error or not.","depth":25,"bounds":{"left":0.017287234,"top":0.6528332,"width":0.19581117,"height":0.035913806},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.17719415,"top":0.65363127,"width":0.0013297872,"height":0.015961692}},{"char_start":1,"char_count":69,"bounds":{"left":0.017287234,"top":0.65363127,"width":0.19614361,"height":0.035115723}}],"role_description":"text"},{"role":"AXHeading","text":"What this means","depth":24,"bounds":{"left":0.01462766,"top":0.7086991,"width":0.22240691,"height":0.021548284},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"What this means","depth":25,"bounds":{"left":0.017287234,"top":0.7102953,"width":0.048537236,"height":0.018355945},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.7102953,"width":0.0066489363,"height":0.018355945}},{"char_start":1,"char_count":14,"bounds":{"left":0.023936171,"top":0.7102953,"width":0.041888297,"height":0.018355945}}],"role_description":"text"},{"role":"AXStaticText","text":"Your","depth":25,"bounds":{"left":0.017287234,"top":0.73743016,"width":0.012965426,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.73822826,"width":0.0033244682,"height":0.015961692}},{"char_start":1,"char_count":3,"bounds":{"left":0.020611702,"top":0.73822826,"width":0.008643617,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter()","depth":26,"bounds":{"left":0.03158245,"top":0.7390263,"width":0.04886968,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.03158245,"top":0.7390263,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":16,"bounds":{"left":0.034574468,"top":0.7390263,"width":0.046210106,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"cannot rely on","depth":25,"bounds":{"left":0.08178192,"top":0.73743016,"width":0.03756649,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.08178192,"top":0.73822826,"width":0.0013297872,"height":0.015961692}},{"char_start":1,"char_count":13,"bounds":{"left":0.08277926,"top":0.73822826,"width":0.032247342,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":26,"bounds":{"left":0.120678194,"top":0.7390263,"width":0.03158245,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.120678194,"top":0.7390263,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":10,"bounds":{"left":0.12367021,"top":0.7390263,"width":0.028922873,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"from HubSpot.","depth":25,"bounds":{"left":0.15359043,"top":0.73743016,"width":0.038896278,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15392287,"top":0.73822826,"width":0.0009973404,"height":0.015961692}},{"char_start":1,"char_count":12,"bounds":{"left":0.1549202,"top":0.73822826,"width":0.03557181,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"The policy-based fallback is the actual primary path","depth":26,"bounds":{"left":0.017287234,"top":0.73743016,"width":0.20412233,"height":0.035913806},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1924867,"top":0.73822826,"width":0.0039893617,"height":0.015961692}},{"char_start":1,"char_count":51,"bounds":{"left":0.017287234,"top":0.73822826,"width":0.20412233,"height":0.035115723}}],"role_description":"text"},{"role":"AXStaticText","text":", not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.","depth":25,"bounds":{"left":0.017287234,"top":0.7565842,"width":0.20478724,"height":0.055067837},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.12034574,"top":0.7573823,"width":0.0013297872,"height":0.015961692}},{"char_start":1,"char_count":148,"bounds":{"left":0.017287234,"top":0.7573823,"width":0.20511968,"height":0.054269753}}],"role_description":"text"},{"role":"AXStaticText","text":"So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.","depth":25,"bounds":{"left":0.017287234,"top":0.8236233,"width":0.19414894,"height":0.035913806},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.8244214,"width":0.0033244682,"height":0.015961692}},{"char_start":1,"char_count":113,"bounds":{"left":0.017287234,"top":0.8244214,"width":0.19414894,"height":0.035115723}}],"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"bounds":{"left":0.22340426,"top":0.877095,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"bounds":{"left":0.019281914,"top":0.8818835,"width":0.0076462766,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.019614361,"top":0.88268155,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":2,"bounds":{"left":0.021941489,"top":0.88268155,"width":0.004986702,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"public","depth":27,"bounds":{"left":0.019281914,"top":0.9082203,"width":0.016954787,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.019614361,"top":0.90901834,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":5,"bounds":{"left":0.022273935,"top":0.90901834,"width":0.013962766,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.035904255,"top":0.9082203,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"bounds":{"left":0.038896278,"top":0.9082203,"width":0.022606382,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.038896278,"top":0.90901834,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.041888297,"top":0.90901834,"width":0.019614361,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.9082203,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter","depth":27,"bounds":{"left":0.06382979,"top":0.9082203,"width":0.042220745,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.06416223,"top":0.90901834,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":14,"bounds":{"left":0.066821806,"top":0.90901834,"width":0.039228722,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.10571808,"top":0.9082203,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"bounds":{"left":0.1087101,"top":0.9082203,"width":0.025265958,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1087101,"top":0.90901834,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":8,"bounds":{"left":0.11170213,"top":0.90901834,"width":0.022273935,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.13364361,"top":0.9082203,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.13663563,"top":0.9082203,"width":0.0056515955,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.14228724,"top":0.9082203,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.14494681,"top":0.9082203,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14760639,"top":0.9082203,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"bounds":{"left":0.1505984,"top":0.9082203,"width":0.008643617,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.019281914,"top":0.9265762,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.94493216,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// Opportunistic: HubSpot rarely sends Retry-After, but use it if present","depth":27,"bounds":{"left":0.030585106,"top":0.94493216,"width":0.20378989,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.2486702,"top":0.9992019,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9632881,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"bounds":{"left":0.030585106,"top":0.9632881,"width":0.022273935,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05285904,"top":0.9632881,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.055518616,"top":0.9632881,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.9632881,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.061170213,"top":0.9632881,"width":0.014295213,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07513298,"top":0.9632881,"width":0.0056515955,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractHeaders","depth":27,"bounds":{"left":0.080784574,"top":0.9632881,"width":0.039228722,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11968085,"top":0.9632881,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.12267287,"top":0.9632881,"width":0.0056515955,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.12832446,"top":0.9632881,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.13098404,"top":0.9632881,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.980846,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"bounds":{"left":0.030585106,"top":0.980846,"width":0.030917553,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.980846,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.06382979,"top":0.980846,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.066821806,"top":0.980846,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.06948138,"top":0.980846,"width":0.014295213,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.08344415,"top":0.980846,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"headerValue","depth":27,"bounds":{"left":0.08909574,"top":0.980846,"width":0.030917553,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11968085,"top":0.980846,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"bounds":{"left":0.12267287,"top":0.980846,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.14494681,"top":0.980846,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14760639,"top":0.980846,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'Retry-After'","depth":27,"bounds":{"left":0.1505984,"top":0.980846,"width":0.03656915,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.18683511,"top":0.980846,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.18949468,"top":0.980846,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.035904255,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.038896278,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"is_numeric","depth":27,"bounds":{"left":0.04155585,"top":0.9992019,"width":0.02825798,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.06948138,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"bounds":{"left":0.07247341,"top":0.9992019,"width":0.030917553,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.10305851,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.10571808,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"bounds":{"left":0.04155585,"top":0.9992019,"width":0.016954787,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.061170213,"top":0.9992019,"width":0.014295213,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07513298,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"bounds":{"left":0.080784574,"top":0.9992019,"width":0.027925532,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.008643617,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11968085,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"bounds":{"left":0.12267287,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.12533244,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.12832446,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.13098404,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"bounds":{"left":0.13364361,"top":0.9992019,"width":0.008643617,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.14228724,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14494681,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"bounds":{"left":0.14760639,"top":0.9992019,"width":0.030917553,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.17852394,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.18118352,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.18417554,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// Primary path: derive from policyName in response body","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.15658244,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.019614361,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.049867023,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.05285904,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.055518616,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.05851064,"top":0.9992019,"width":0.013962766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07247341,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"bounds":{"left":0.077792555,"top":0.9992019,"width":0.03656915,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.1143617,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.11702128,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.12267287,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.12533244,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.016954787,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04720745,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.049867023,"top":0.9992019,"width":0.014295213,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.06382979,"top":0.9992019,"width":0.005984043,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"bounds":{"left":0.06948138,"top":0.9992019,"width":0.02825798,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.09740692,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"match","depth":27,"bounds":{"left":0.100398935,"top":0.9992019,"width":0.013962766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1143617,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11702128,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"bounds":{"left":0.11968085,"top":0.9992019,"width":0.019946808,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.1392952,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14228724,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.14494681,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'SECONDLY'","depth":27,"bounds":{"left":0.04155585,"top":0.9992019,"width":0.02825798,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.06948138,"top":0.9992019,"width":0.03656915,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.9992019,"width":0.005984043,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"bounds":{"left":0.1143617,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.11702128,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'TEN_SECONDLY_ROLLING'","depth":27,"bounds":{"left":0.04155585,"top":0.9992019,"width":0.061835106,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.10305851,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.9992019,"width":0.005984043,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"8","depth":27,"bounds":{"left":0.1143617,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.11702128,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'DAILY'","depth":27,"bounds":{"left":0.04155585,"top":0.9992019,"width":0.019946808,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.9992019,"width":0.04488032,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.9992019,"width":0.005984043,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3600","depth":27,"bounds":{"left":0.1143617,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.12533244,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"default","depth":27,"bounds":{"left":0.04155585,"top":0.9992019,"width":0.019946808,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.9992019,"width":0.04488032,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.9992019,"width":0.005984043,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":27,"bounds":{"left":0.1143617,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.11702128,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.03324468,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.035904255,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.019614361,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.038896278,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"bounds":{"left":0.04155585,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.06382979,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"bounds":{"left":0.066821806,"top":0.9992019,"width":0.03656915,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.10305851,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"bounds":{"left":0.10571808,"top":0.9992019,"width":0.025598405,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.13098404,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.13364361,"top":0.9992019,"width":0.005984043,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.1392952,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.14228724,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14494681,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"bounds":{"left":0.14760639,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"string","depth":27,"bounds":{"left":0.1505984,"top":0.9992019,"width":0.016954787,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.013962766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04454787,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.055518616,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.061170213,"top":0.9992019,"width":0.014295213,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07513298,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractBody","depth":27,"bounds":{"left":0.080784574,"top":0.9992019,"width":0.030917553,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.1143617,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.11968085,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.12267287,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.022273935,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05285904,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.055518616,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"json_decode","depth":27,"bounds":{"left":0.061170213,"top":0.9992019,"width":0.030917553,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.091755316,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"bounds":{"left":0.09474734,"top":0.9992019,"width":0.013962766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"true","depth":27,"bounds":{"left":0.1143617,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.12533244,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.12832446,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"bounds":{"left":0.13098404,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.13364361,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.13663563,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"bounds":{"left":0.1392952,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"bounds":{"left":0.14228724,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.14494681,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.016954787,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04720745,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"strtoupper","depth":27,"bounds":{"left":0.049867023,"top":0.9992019,"width":0.02825798,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.077792555,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"bounds":{"left":0.080784574,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"bounds":{"left":0.10305851,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'policyName'","depth":27,"bounds":{"left":0.10571808,"top":0.9992019,"width":0.033909574,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"bounds":{"left":0.1392952,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14228724,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"bounds":{"left":0.14494681,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1505984,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"''","depth":27,"bounds":{"left":0.15325798,"top":0.9992019,"width":0.005984043,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.15890957,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.16156915,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"bounds":{"left":0.16456117,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.16722074,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.17021276,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"null","depth":27,"bounds":{"left":0.17287233,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.18417554,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.019614361,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.038896278,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"bounds":{"left":0.04155585,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.06382979,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"bounds":{"left":0.066821806,"top":0.9992019,"width":0.027925532,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.09474734,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"bounds":{"left":0.09740692,"top":0.9992019,"width":0.008643617,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.10571808,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.13098404,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.13364361,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.13663563,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"bounds":{"left":0.1392952,"top":0.9992019,"width":0.008643617,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// ±20% randomization to prevent thundering-herd retries","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.15658244,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.019614361,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.049867023,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.05285904,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.055518616,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.05851064,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"bounds":{"left":0.061170213,"top":0.9992019,"width":0.008643617,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.06948138,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.07247341,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.07513298,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"bounds":{"left":0.077792555,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.100398935,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"*","depth":27,"bounds":{"left":0.10305851,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.10571808,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0.2","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.008643617,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.11702128,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.11968085,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.016954787,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04720745,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"bounds":{"left":0.049867023,"top":0.9992019,"width":0.008643617,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.05851064,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"bounds":{"left":0.061170213,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.06382979,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.066821806,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"bounds":{"left":0.06948138,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.091755316,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":27,"bounds":{"left":0.09474734,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.09740692,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random_int","depth":27,"bounds":{"left":0.100398935,"top":0.9992019,"width":0.027925532,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.12832446,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":27,"bounds":{"left":0.13098404,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"bounds":{"left":0.13364361,"top":0.9992019,"width":0.019946808,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.15325798,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.15625,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"bounds":{"left":0.15890957,"top":0.9992019,"width":0.019614361,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.17852394,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.18118352,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.18417554,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Why those defaults","depth":24,"bounds":{"left":0.01462766,"top":0.9992019,"width":0.22240691,"height":0.0007980846},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Why those defaults","depth":25,"bounds":{"left":0.017287234,"top":0.9992019,"width":0.05518617,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SECONDLY","depth":28,"bounds":{"left":0.02925532,"top":0.9992019,"width":0.023271276,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ 2 seconds.","depth":27,"bounds":{"left":0.053856384,"top":0.9992019,"width":0.032247342,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.","depth":26,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.18783244,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TEN_SECONDLY_ROLLING","depth":28,"bounds":{"left":0.02925532,"top":0.9992019,"width":0.057513297,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ 8 seconds.","depth":27,"bounds":{"left":0.08809841,"top":0.9992019,"width":0.032579787,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The window is 10 seconds","depth":26,"bounds":{"left":0.120678194,"top":0.9992019,"width":0.06549202,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"rolling","depth":27,"bounds":{"left":0.1861702,"top":0.9992019,"width":0.015957447,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":". If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.","depth":26,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.1974734,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"DAILY","depth":28,"bounds":{"left":0.02925532,"top":0.9992019,"width":0.01462766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ 3600 (1 hour) or push to next-day queue.","depth":27,"bounds":{"left":0.045212764,"top":0.9992019,"width":0.1087101,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)","depth":26,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.19714096,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jitter","depth":27,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.013962766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.","depth":26,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.1981383,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Bonus: log the correlation ID","depth":24,"bounds":{"left":0.01462766,"top":0.9992019,"width":0.22240691,"height":0.0007980846},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Bonus: log the correlation ID","depth":25,"bounds":{"left":0.017287234,"top":0.9992019,"width":0.08111702,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"You're throwing away a useful debugging signal. The","depth":25,"bounds":{"left":0.017287234,"top":0.9992019,"width":0.1299867,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":26,"bounds":{"left":0.14860372,"top":0.9992019,"width":0.069148935,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support \"why did this specific call get rate limited\" or correlate with their internal logs if you ever need to escalate. Capture it:","depth":25,"bounds":{"left":0.017287234,"top":0.9992019,"width":0.20711437,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"bounds":{"left":0.22340426,"top":0.9992019,"width":0.010638298,"height":0.0007980846},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.0076462766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$correlationId","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.039228722,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.061170213,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.06382979,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.066821806,"top":0.9992019,"width":0.013962766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.080784574,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"headerValue","depth":27,"bounds":{"left":0.08643617,"top":0.9992019,"width":0.030917553,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11702128,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"bounds":{"left":0.11968085,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.14228724,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14494681,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'x-hubspot-correlation-id'","depth":27,"bounds":{"left":0.14760639,"top":0.9992019,"width":0.07280585,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.22041224,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.035904255,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.038896278,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"bounds":{"left":0.04155585,"top":0.9992019,"width":0.022606382,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"bounds":{"left":0.06382979,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'correlationId'","depth":27,"bounds":{"left":0.066821806,"top":0.9992019,"width":0.041888297,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"bounds":{"left":0.1143617,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.11968085,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"null","depth":27,"bounds":{"left":0.12267287,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.13364361,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.13663563,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.013962766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.03324468,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"log","depth":27,"bounds":{"left":0.038896278,"top":0.9992019,"width":0.008643617,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.04720745,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"warning","depth":27,"bounds":{"left":0.05285904,"top":0.9992019,"width":0.019614361,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.07247341,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'[Hubspot] 429 received'","depth":27,"bounds":{"left":0.07513298,"top":0.9992019,"width":0.06715426,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.14228724,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14494681,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"bounds":{"left":0.14760639,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'policy'","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.022273935,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05285904,"top":0.9992019,"width":0.050531916,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10305851,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.019946808,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.13098404,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'retry_after'","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.036236703,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.066821806,"top":0.9992019,"width":0.03656915,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10305851,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$delaySeconds","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.03656915,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.14760639,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'hubspot_correlation_id'","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.06715426,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.09740692,"top":0.9992019,"width":0.005984043,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10305851,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$correlationId","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.039228722,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.1505984,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.011303191,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'endpoint'","depth":27,"bounds":{"left":0.030585106,"top":0.9992019,"width":0.027925532,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.9992019,"width":0.04488032,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10305851,"top":0.9992019,"width":0.0056515955,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1087101,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$endpointForLogging","depth":27,"bounds":{"left":0.111369684,"top":0.9992019,"width":0.05319149,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.16456117,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.16722074,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// pass this in","depth":27,"bounds":{"left":0.17021276,"top":0.9992019,"width":0.041888297,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"bounds":{"left":0.019281914,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.021941489,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.024933511,"top":0.9992019,"width":0.0029920214,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"When a customer reports \"my sync was slow today\" and you can correlate to a flurry of 429s with HubSpot correlation IDs, the diagnosis path is much shorter.","depth":25,"bounds":{"left":0.017287234,"top":0.9992019,"width":0.2044548,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"So the final picture","depth":24,"bounds":{"left":0.01462766,"top":0.9992019,"width":0.22240691,"height":0.0007980846},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"So the final picture","depth":25,"bounds":{"left":0.017287234,"top":0.9992019,"width":0.053856384,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"For this codebase, given what HubSpot actually sends:","depth":25,"bounds":{"left":0.017287234,"top":0.9992019,"width":0.13331117,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Pre-acquire from Redis bucket before every call","depth":27,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.1200133,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"— this is your primary defense, since post-hoc info is sparse.","depth":26,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.19714096,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"On 429, read","depth":27,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.03324468,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":28,"bounds":{"left":0.0625,"top":0.9992019,"width":0.028922873,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"from the body","depth":27,"bounds":{"left":0.09275266,"top":0.9992019,"width":0.036901597,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"— the only reliable signal of which bucket fired.","depth":26,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.18716756,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Apply policy-specific backoff with jitter","depth":27,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.100398935,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":26,"bounds":{"left":0.12799202,"top":0.9992019,"width":0.0076462766,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"bounds":{"left":0.13696809,"top":0.9992019,"width":0.031914894,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"is nice-to-have but rare in practice.","depth":26,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.18916224,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"On success, capture","depth":27,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.05119681,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":28,"bounds":{"left":0.08045213,"top":0.9992019,"width":0.06050532,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"headers via Guzzle middleware","depth":27,"bounds":{"left":0.14228724,"top":0.9992019,"width":0.080784574,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"to clamp the local counter — these","depth":26,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.08510638,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"are","depth":27,"bounds":{"left":0.11303192,"top":0.9992019,"width":0.007978723,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"present on 200 responses (just confirmed in your earlier Postman screenshot showing","depth":26,"bounds":{"left":0.027925532,"top":0.9992019,"width":0.19514628,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Max=110","depth":27,"bounds":{"left":0.13796543,"top":0.9992019,"width":0.020279255,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":").","depth":26,"bounds":{"left":0.15957446,"top":0.9992019,"width":0.0033244682,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The asymmetry is the key insight: HubSpot tells you a lot when things are fine, almost nothing when they're not. Your limiter has to be smart enough to work with that.","depth":25,"bounds":{"left":0.017287234,"top":0.9992019,"width":0.2017952,"height":0.0007980846},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":22,"bounds":{"left":0.01462766,"top":0.9992019,"width":0.010638298,"height":0.0007980846},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":22,"bounds":{"left":0.025265958,"top":0.9992019,"width":0.010638298,"height":0.0007980846},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":22,"bounds":{"left":0.035904255,"top":0.9992019,"width":0.010638298,"height":0.0007980846},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":22,"bounds":{"left":0.04654255,"top":0.9992019,"width":0.010638298,"height":0.0007980846},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scroll to bottom","depth":21,"bounds":{"left":0.11968085,"top":0.8475658,"width":0.011968086,"height":0.02952913},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Write a message…","depth":25,"bounds":{"left":0.016289894,"top":0.90901834,"width":0.22174202,"height":0.018355945},"on_screen":true,"value":"Write a message…","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Write a message…","depth":28,"bounds":{"left":0.016289894,"top":0.90981644,"width":0.04654255,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add files, connectors, and more","depth":27,"bounds":{"left":0.014960106,"top":0.93695134,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Model: Opus 4.7 Adaptive","depth":27,"bounds":{"left":0.16156915,"top":0.93695134,"width":0.052526597,"height":0.025538707},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Opus 4.7","depth":29,"bounds":{"left":0.16489361,"top":0.9425379,"width":0.019281914,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Adaptive","depth":28,"bounds":{"left":0.1861702,"top":0.9425379,"width":0.019946808,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Settings","depth":26,"bounds":{"left":0.21808511,"top":0.9385475,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Press and hold to record","depth":25,"bounds":{"left":0.22739361,"top":0.9385475,"width":0.010638298,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":22,"bounds":{"left":0.061502658,"top":0.980846,"width":0.12832446,"height":0.011971269},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":23,"bounds":{"left":0.061502658,"top":0.980846,"width":0.12832446,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Preview","depth":20,"bounds":{"left":0.25831118,"top":0.035913806,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Code","depth":20,"bounds":{"left":0.26761967,"top":0.035913806,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Hubspot rate limits reference · MD","depth":19,"bounds":{"left":0.28025267,"top":0.039106146,"width":0.14960106,"height":0.015961692},"on_screen":true,"help_text":"Hubspot rate limits reference","role_description":"heading"},{"role":"AXStaticText","text":"Hubspot rate limits reference","depth":20,"bounds":{"left":0.28025267,"top":0.03990423,"width":0.06416223,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"·","depth":20,"bounds":{"left":0.3444149,"top":0.03990423,"width":0.0033244682,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MD","depth":20,"bounds":{"left":0.34740692,"top":0.03990423,"width":0.007978723,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":19,"bounds":{"left":0.44414893,"top":0.03431764,"width":0.015625,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go back","depth":19,"bounds":{"left":0.48271278,"top":0.032721467,"width":0.011968086,"height":0.028731046},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"HubSpot Rate Limit Reference","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"HubSpot Rate Limit Reference","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Quick reference for checking what rate limits apply to a portal.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Three buckets, three discovery methods","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Three buckets, three discovery methods","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"}]...
|
-5939253293304560147
|
-7923152015067362150
|
idle
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''
)
?
:
null
;
}
private
function
withJitter
(
int
$seconds
)
:
int
{
// ±20% randomization to prevent thundering-herd retries
$jitter
=
(
int
)
(
$seconds
*
0.2
)
;
return
max
(
1
,
$seconds
+
random_int
(
-
$jitter
,
$jitter
)
)
;
}
Why those defaults
Why those defaults
SECONDLY
→ 2 seconds.
The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.
TEN_SECONDLY_ROLLING
→ 8 seconds.
The window is 10 seconds
rolling
. If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.
DAILY
→ 3600 (1 hour) or push to next-day queue.
Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)
Jitter
is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.
Bonus: log the correlation ID
Bonus: log the correlation ID
You're throwing away a useful debugging signal. The
x-hubspot-correlation-id
from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support "why did this specific call get rate limited" or correlate with their internal logs if you ever need to escalate. Capture it:
Copy to clipboard
php
$correlationId
=
$this
->
headerValue
(
$headers
,
'x-hubspot-correlation-id'
)
??
(
$payload
[
'correlationId'
]
??
null
)
;
$this
->
log
->
warning
(
'[Hubspot] 429 received'
,
[
'policy'
=>
$policy
,
'retry_after'
=>
$delaySeconds
,
'hubspot_correlation_id'
=>
$correlationId
,
'endpoint'
=>
$endpointForLogging
,
// pass this in
]
)
;
When a customer reports "my sync was slow today" and you can correlate to a flurry of 429s with HubSpot correlation IDs, the diagnosis path is much shorter.
So the final picture
So the final picture
For this codebase, given what HubSpot actually sends:
Pre-acquire from Redis bucket before every call
— this is your primary defense, since post-hoc info is sparse.
On 429, read
policyName
from the body
— the only reliable signal of which bucket fired.
Apply policy-specific backoff with jitter
—
Retry-After
is nice-to-have but rare in practice.
On success, capture
X-HubSpot-RateLimit-*
headers via Guzzle middleware
to clamp the local counter — these
are
present on 200 responses (just confirmed in your earlier Postman screenshot showing
Max=110
).
The asymmetry is the key insight: HubSpot tells you a lot when things are fine, almost nothing when they're not. Your limiter has to be smart enough to work with that.
Copy
Give positive feedback
Give negative feedback
Retry
Scroll to bottom
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Settings
Press and hold to record
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4588
|
NULL
|
0
|
2026-05-07T14:11:51.409238+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163111409_m1.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''
)
?
:
null
;
}
private
function
withJitter
(
int
$seconds
)
:
int
{
// ±20% randomization to prevent thundering-herd retries
$jitter
=
(
int
)
(
$seconds
*
0.2
)
;
return
max
(
1
,
$seconds
+
random_int
(
-
$jitter
,
$jitter
)
)
;
}
Why those defaults
Why those defaults
SECONDLY
→ 2 seconds.
The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.
TEN_SECONDLY_ROLLING
→ 8 seconds.
The window is 10 seconds
rolling
. If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.
DAILY
→ 3600 (1 hour) or push to next-day queue.
Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)
Jitter
is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.
Bonus: log the correlation ID
Bonus: log the correlation ID
You're throwing away a useful debugging signal. The
x-hubspot-correlation-id
from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support "why did this specific call get rate limited" or correlate with their internal logs if you ever need to escalate. Capture it:
Copy to clipboard
php
$correlationId
=
$this
->
headerValue
(
$headers
,
'x-hubspot-correlation-id'
)
??
(
$payload
[
'correlationId'
]
??
null
)
;
$this
->
log
->
warning
(
'[Hubspot] 429 received'
,
[
'policy'
=>
$policy
,
'retry_after'
=>
$delaySeconds
,
'hubspot_correlation_id'
=>
$correlationId
,
'endpoint'
=>
$endpointForLogging
,
// pass this in
]
)
;
When a customer reports "my sync was slow today" and you can correlate to a flurry of 429s with HubSpot correlation IDs, the diagnosis path is much shorter.
So the final picture
So the final picture
For this codebase, given what HubSpot actually sends:
Pre-acquire from Redis bucket before every call
— this is your primary defense, since post-hoc info is sparse.
On 429, read
policyName
from the body
— the only reliable signal of which bucket fired.
Apply policy-specific backoff with jitter
—
Retry-After
is nice-to-have but rare in practice.
On success, capture
X-HubSpot-RateLimit-*
headers via Guzzle middleware
to clamp the local counter — these
are
present on 200 responses (just confirmed in your earlier Postman screenshot showing
Max=110
).
The asymmetry is the key insight: HubSpot tells you a lot when things are fine, almost nothing when they're not. Your limiter has to be smart enough to work with that.
Copy
Give positive feedback
Give negative feedback
Retry
Scroll to bottom
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Settings
Press and hold to record
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous version","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 / 2","depth":21,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Next version","depth":21,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: That confirms the diagnosis.","depth":21,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: That confirms the diagnosis.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"That confirms the diagnosis. Look at what's there and — more importantly — what isn't:","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Missing from the 429 response:","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Present that's actually useful:","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"✅","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(for debugging / filing tickets with HubSpot)","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"search","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"429 (the","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"/contact/search","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"one) — search responses always strip rate-limit headers, error or not.","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"What this means","depth":24,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"What this means","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Your","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter()","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"cannot rely on","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"from HubSpot.","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The policy-based fallback is the actual primary path","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":", not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"public","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// Opportunistic: HubSpot rarely sends Retry-After, but use it if present","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.0,"top":0.0,"width":0.00069444446,"height":0.0011111111},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractHeaders","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"headerValue","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'Retry-After'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"is_numeric","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// Primary path: derive from policyName in response body","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"match","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'SECONDLY'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'TEN_SECONDLY_ROLLING'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"8","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'DAILY'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3600","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"default","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"string","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractBody","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"json_decode","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"true","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"strtoupper","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'policyName'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"''","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"null","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// ±20% randomization to prevent thundering-herd retries","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"*","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0.2","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random_int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Why those defaults","depth":24,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Why those defaults","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SECONDLY","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ 2 seconds.","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TEN_SECONDLY_ROLLING","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ 8 seconds.","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The window is 10 seconds","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"rolling","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":". If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"DAILY","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ 3600 (1 hour) or push to next-day queue.","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Bonus: log the correlation ID","depth":24,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Bonus: log the correlation ID","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"You're throwing away a useful debugging signal. The","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support \"why did this specific call get rate limited\" or correlate with their internal logs if you ever need to escalate. Capture it:","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.0011111111},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$correlationId","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"headerValue","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'x-hubspot-correlation-id'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'correlationId'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"null","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"log","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"warning","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'[Hubspot] 429 received'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'policy'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'retry_after'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$delaySeconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'hubspot_correlation_id'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$correlationId","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'endpoint'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$endpointForLogging","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// pass this in","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"When a customer reports \"my sync was slow today\" and you can correlate to a flurry of 429s with HubSpot correlation IDs, the diagnosis path is much shorter.","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"So the final picture","depth":24,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"So the final picture","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"For this codebase, given what HubSpot actually sends:","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Pre-acquire from Redis bucket before every call","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"— this is your primary defense, since post-hoc info is sparse.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"On 429, read","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"from the body","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"— the only reliable signal of which bucket fired.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Apply policy-specific backoff with jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"is nice-to-have but rare in practice.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"On success, capture","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"headers via Guzzle middleware","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"to clamp the local counter — these","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"are","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"present on 200 responses (just confirmed in your earlier Postman screenshot showing","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Max=110","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":").","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The asymmetry is the key insight: HubSpot tells you a lot when things are fine, almost nothing when they're not. Your limiter has to be smart enough to work with that.","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scroll to bottom","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Write a message…","depth":25,"on_screen":true,"value":"Write a message…","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Write a message…","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add files, connectors, and more","depth":27,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Model: Opus 4.7 Adaptive","depth":27,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Opus 4.7","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Adaptive","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Settings","depth":26,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Press and hold to record","depth":25,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.031111112},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":22,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Preview","depth":20,"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Code","depth":20,"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Hubspot rate limits reference · MD","depth":19,"on_screen":true,"help_text":"Hubspot rate limits reference","role_description":"heading"},{"role":"AXStaticText","text":"Hubspot rate limits reference","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"·","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MD","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go back","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"HubSpot Rate Limit Reference","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"HubSpot Rate Limit Reference","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Quick reference for checking what rate limits apply to a portal.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Three buckets, three discovery methods","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Three buckets, three discovery methods","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"}]...
|
-8389449794491108425
|
-3311465996631585638
|
idle
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''
)
?
:
null
;
}
private
function
withJitter
(
int
$seconds
)
:
int
{
// ±20% randomization to prevent thundering-herd retries
$jitter
=
(
int
)
(
$seconds
*
0.2
)
;
return
max
(
1
,
$seconds
+
random_int
(
-
$jitter
,
$jitter
)
)
;
}
Why those defaults
Why those defaults
SECONDLY
→ 2 seconds.
The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.
TEN_SECONDLY_ROLLING
→ 8 seconds.
The window is 10 seconds
rolling
. If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.
DAILY
→ 3600 (1 hour) or push to next-day queue.
Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)
Jitter
is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.
Bonus: log the correlation ID
Bonus: log the correlation ID
You're throwing away a useful debugging signal. The
x-hubspot-correlation-id
from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support "why did this specific call get rate limited" or correlate with their internal logs if you ever need to escalate. Capture it:
Copy to clipboard
php
$correlationId
=
$this
->
headerValue
(
$headers
,
'x-hubspot-correlation-id'
)
??
(
$payload
[
'correlationId'
]
??
null
)
;
$this
->
log
->
warning
(
'[Hubspot] 429 received'
,
[
'policy'
=>
$policy
,
'retry_after'
=>
$delaySeconds
,
'hubspot_correlation_id'
=>
$correlationId
,
'endpoint'
=>
$endpointForLogging
,
// pass this in
]
)
;
When a customer reports "my sync was slow today" and you can correlate to a flurry of 429s with HubSpot correlation IDs, the diagnosis path is much shorter.
So the final picture
So the final picture
For this codebase, given what HubSpot actually sends:
Pre-acquire from Redis bucket before every call
— this is your primary defense, since post-hoc info is sparse.
On 429, read
policyName
from the body
— the only reliable signal of which bucket fired.
Apply policy-specific backoff with jitter
—
Retry-After
is nice-to-have but rare in practice.
On success, capture
X-HubSpot-RateLimit-*
headers via Guzzle middleware
to clamp the local counter — these
are
present on 200 responses (just confirmed in your earlier Postman screenshot showing
Max=110
).
The asymmetry is the key insight: HubSpot tells you a lot when things are fine, almost nothing when they're not. Your limiter has to be smart enough to work with that.
Copy
Give positive feedback
Give negative feedback
Retry
Scroll to bottom
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Settings
Press and hold to record
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call...
|
4586
|
NULL
|
NULL
|
NULL
|
|
4589
|
NULL
|
0
|
2026-05-07T14:12:04.390097+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163124390_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous version","depth":21,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 / 2","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Next version","depth":21,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: That confirms the diagnosis.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: That confirms the diagnosis.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"That confirms the diagnosis. Look at what's there and — more importantly — what isn't:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Missing from the 429 response:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.019952115,"width":0.0063164895,"height":0.0103751},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":27,"bounds":{"left":0.03557181,"top":0.019952115,"width":0.06615692,"height":0.009577015},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.03830806,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"bounds":{"left":0.03557181,"top":0.03990423,"width":0.08344415,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.03990423,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":28,"bounds":{"left":0.03856383,"top":0.03990423,"width":0.080784574,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"bounds":{"left":0.027925532,"top":0.06304868,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":27,"bounds":{"left":0.03557181,"top":0.06464485,"width":0.11801862,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.06464485,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":40,"bounds":{"left":0.03856383,"top":0.06464485,"width":0.11502659,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"Present that's actually useful:","depth":26,"bounds":{"left":0.017287234,"top":0.10295291,"width":0.07480053,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.103751,"width":0.003656915,"height":0.015961692}},{"char_start":1,"char_count":30,"bounds":{"left":0.020944148,"top":0.103751,"width":0.07114362,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"✅","depth":26,"bounds":{"left":0.027925532,"top":0.13248204,"width":0.0063164895,"height":0.016759777},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":27,"bounds":{"left":0.03557181,"top":0.13407822,"width":0.069148935,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.035904255,"top":0.13407822,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":23,"bounds":{"left":0.03856383,"top":0.13407822,"width":0.06648936,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"(for debugging / filing tickets with HubSpot)","depth":26,"bounds":{"left":0.10605053,"top":0.13248204,"width":0.11037234,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10638298,"top":0.13328013,"width":0.0009973404,"height":0.015961692}},{"char_start":1,"char_count":44,"bounds":{"left":0.107380316,"top":0.13328013,"width":0.10704787,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the","depth":25,"bounds":{"left":0.017287234,"top":0.17238627,"width":0.20678191,"height":0.055067837},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.17318435,"width":0.0033244682,"height":0.015961692}},{"char_start":1,"char_count":196,"bounds":{"left":0.017287234,"top":0.17318435,"width":0.20678191,"height":0.054269753}}],"role_description":"text"},{"role":"AXStaticText","text":"search","depth":26,"bounds":{"left":0.091755316,"top":0.21069433,"width":0.016954787,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.091755316,"top":0.21149242,"width":0.0029920214,"height":0.015961692}},{"char_start":1,"char_count":5,"bounds":{"left":0.0944149,"top":0.21149242,"width":0.01462766,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"429 (the","depth":25,"bounds":{"left":0.10837766,"top":0.21069433,"width":0.022606382,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1087101,"top":0.21149242,"width":0.0009973404,"height":0.015961692}},{"char_start":1,"char_count":7,"bounds":{"left":0.109707445,"top":0.21149242,"width":0.01761968,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"/contact/search","depth":26,"bounds":{"left":0.13231383,"top":0.2122905,"width":0.043550532,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.13264628,"top":0.2122905,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":14,"bounds":{"left":0.13530585,"top":0.2122905,"width":0.04055851,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"one) — search responses always strip rate-limit headers, error or not.","depth":25,"bounds":{"left":0.017287234,"top":0.21069433,"width":0.19581117,"height":0.035913806},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.17719415,"top":0.21149242,"width":0.0013297872,"height":0.015961692}},{"char_start":1,"char_count":69,"bounds":{"left":0.017287234,"top":0.21149242,"width":0.19614361,"height":0.035115723}}],"role_description":"text"},{"role":"AXHeading","text":"What this means","depth":24,"bounds":{"left":0.01462766,"top":0.26656026,"width":0.22240691,"height":0.021548284},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"What this means","depth":25,"bounds":{"left":0.017287234,"top":0.26815644,"width":0.048537236,"height":0.018355945},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.26815644,"width":0.0066489363,"height":0.018355945}},{"char_start":1,"char_count":14,"bounds":{"left":0.023936171,"top":0.26815644,"width":0.041888297,"height":0.018355945}}],"role_description":"text"},{"role":"AXStaticText","text":"Your","depth":25,"bounds":{"left":0.017287234,"top":0.2952913,"width":0.012965426,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.29608938,"width":0.0033244682,"height":0.015961692}},{"char_start":1,"char_count":3,"bounds":{"left":0.020611702,"top":0.29608938,"width":0.008643617,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter()","depth":26,"bounds":{"left":0.03158245,"top":0.29688746,"width":0.04886968,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.03158245,"top":0.29688746,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":16,"bounds":{"left":0.034574468,"top":0.29688746,"width":0.046210106,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"cannot rely on","depth":25,"bounds":{"left":0.08178192,"top":0.2952913,"width":0.03756649,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.08178192,"top":0.29608938,"width":0.0013297872,"height":0.015961692}},{"char_start":1,"char_count":13,"bounds":{"left":0.08277926,"top":0.29608938,"width":0.032247342,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":26,"bounds":{"left":0.120678194,"top":0.29688746,"width":0.03158245,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.120678194,"top":0.29688746,"width":0.0029920214,"height":0.015163607}},{"char_start":1,"char_count":10,"bounds":{"left":0.12367021,"top":0.29688746,"width":0.028922873,"height":0.015163607}}],"role_description":"text"},{"role":"AXStaticText","text":"from HubSpot.","depth":25,"bounds":{"left":0.15359043,"top":0.2952913,"width":0.038896278,"height":0.016759777},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15392287,"top":0.29608938,"width":0.0009973404,"height":0.015961692}},{"char_start":1,"char_count":12,"bounds":{"left":0.1549202,"top":0.29608938,"width":0.03557181,"height":0.015961692}}],"role_description":"text"},{"role":"AXStaticText","text":"The policy-based fallback is the actual primary path","depth":26,"bounds":{"left":0.017287234,"top":0.2952913,"width":0.20412233,"height":0.035913806},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1924867,"top":0.29608938,"width":0.0039893617,"height":0.015961692}},{"char_start":1,"char_count":51,"bounds":{"left":0.017287234,"top":0.29608938,"width":0.20412233,"height":0.035115723}}],"role_description":"text"},{"role":"AXStaticText","text":", not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.","depth":25,"bounds":{"left":0.017287234,"top":0.31444532,"width":0.20478724,"height":0.055067837},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.12034574,"top":0.31524342,"width":0.0013297872,"height":0.015961692}},{"char_start":1,"char_count":148,"bounds":{"left":0.017287234,"top":0.31524342,"width":0.20511968,"height":0.054269753}}],"role_description":"text"},{"role":"AXStaticText","text":"So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.","depth":25,"bounds":{"left":0.017287234,"top":0.38148445,"width":0.19414894,"height":0.035913806},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.38228253,"width":0.0033244682,"height":0.015961692}},{"char_start":1,"char_count":113,"bounds":{"left":0.017287234,"top":0.38228253,"width":0.19414894,"height":0.035115723}}],"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"bounds":{"left":0.22340426,"top":0.4349561,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"bounds":{"left":0.019281914,"top":0.43974462,"width":0.0076462766,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.019614361,"top":0.4405427,"width":0.0023271276,"height":0.011971269}},{"char_start":1,"char_count":2,"bounds":{"left":0.021941489,"top":0.4405427,"width":0.004986702,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"public","depth":27,"bounds":{"left":0.019281914,"top":0.4660814,"width":0.016954787,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.019614361,"top":0.4668795,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":5,"bounds":{"left":0.022273935,"top":0.4668795,"width":0.013962766,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.035904255,"top":0.4660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"bounds":{"left":0.038896278,"top":0.4660814,"width":0.022606382,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.038896278,"top":0.4668795,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.041888297,"top":0.4668795,"width":0.019614361,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.4660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter","depth":27,"bounds":{"left":0.06382979,"top":0.4660814,"width":0.042220745,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.06416223,"top":0.4668795,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":14,"bounds":{"left":0.066821806,"top":0.4668795,"width":0.039228722,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.10571808,"top":0.4660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"bounds":{"left":0.1087101,"top":0.4660814,"width":0.025265958,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1087101,"top":0.4668795,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":8,"bounds":{"left":0.11170213,"top":0.4668795,"width":0.022273935,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.13364361,"top":0.4660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.13663563,"top":0.4660814,"width":0.0056515955,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.13663563,"top":0.4668795,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.13962767,"top":0.4668795,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.14228724,"top":0.4660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.14494681,"top":0.4660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14760639,"top":0.4660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"bounds":{"left":0.1505984,"top":0.4660814,"width":0.008643617,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1505984,"top":0.4668795,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":2,"bounds":{"left":0.15359043,"top":0.4668795,"width":0.0056515955,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.019281914,"top":0.48443735,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.5027933,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// Opportunistic: HubSpot rarely sends Retry-After, but use it if present","depth":27,"bounds":{"left":0.030585106,"top":0.5027933,"width":0.20378989,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.030585106,"top":0.5027933,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":72,"bounds":{"left":0.03357713,"top":0.5027933,"width":0.20113032,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.2486702,"top":0.9114126,"width":0.0003324468,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.5211492,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"bounds":{"left":0.030585106,"top":0.5211492,"width":0.022273935,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.030585106,"top":0.5211492,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.03357713,"top":0.5211492,"width":0.019614361,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05285904,"top":0.5211492,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.055518616,"top":0.5211492,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.5211492,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.061170213,"top":0.5211492,"width":0.014295213,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.061502658,"top":0.5211492,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":4,"bounds":{"left":0.06416223,"top":0.5211492,"width":0.011303191,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07513298,"top":0.5211492,"width":0.0056515955,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.075465426,"top":0.5211492,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.078125,"top":0.5211492,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"extractHeaders","depth":27,"bounds":{"left":0.080784574,"top":0.5211492,"width":0.039228722,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.080784574,"top":0.5211492,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":13,"bounds":{"left":0.08377659,"top":0.5211492,"width":0.036236703,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11968085,"top":0.5211492,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.12267287,"top":0.5211492,"width":0.0056515955,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.12267287,"top":0.5211492,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.12566489,"top":0.5211492,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.12832446,"top":0.5211492,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.13098404,"top":0.5211492,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.5387071,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"bounds":{"left":0.030585106,"top":0.5387071,"width":0.030917553,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.030585106,"top":0.5395052,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":10,"bounds":{"left":0.03357713,"top":0.5395052,"width":0.027925532,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.5387071,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.06382979,"top":0.5387071,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.066821806,"top":0.5387071,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.06948138,"top":0.5387071,"width":0.014295213,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.06981383,"top":0.5395052,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":4,"bounds":{"left":0.07247341,"top":0.5395052,"width":0.011303191,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.08344415,"top":0.5387071,"width":0.005984043,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.08377659,"top":0.5395052,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.08643617,"top":0.5395052,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"headerValue","depth":27,"bounds":{"left":0.08909574,"top":0.5387071,"width":0.030917553,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11968085,"top":0.5387071,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"bounds":{"left":0.12267287,"top":0.5387071,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.14494681,"top":0.5387071,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14760639,"top":0.5387071,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'Retry-After'","depth":27,"bounds":{"left":0.1505984,"top":0.5387071,"width":0.03656915,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.18683511,"top":0.5387071,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.18949468,"top":0.5387071,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.55706304,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":27,"bounds":{"left":0.030585106,"top":0.55706304,"width":0.0056515955,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.035904255,"top":0.55706304,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.038896278,"top":0.55706304,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"is_numeric","depth":27,"bounds":{"left":0.04155585,"top":0.55706304,"width":0.02825798,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.06948138,"top":0.55706304,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"bounds":{"left":0.07247341,"top":0.55706304,"width":0.030917553,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.10305851,"top":0.55706304,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.10571808,"top":0.55706304,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1087101,"top":0.55706304,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.111369684,"top":0.55706304,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.575419,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"bounds":{"left":0.04155585,"top":0.575419,"width":0.016954787,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.061170213,"top":0.575419,"width":0.014295213,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07513298,"top":0.575419,"width":0.0056515955,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"bounds":{"left":0.080784574,"top":0.575419,"width":0.027925532,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.1087101,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"bounds":{"left":0.111369684,"top":0.575419,"width":0.008643617,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11968085,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"bounds":{"left":0.12267287,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.12533244,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.12832446,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.13098404,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"bounds":{"left":0.13364361,"top":0.575419,"width":0.008643617,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.14228724,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14494681,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"bounds":{"left":0.14760639,"top":0.575419,"width":0.030917553,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.17852394,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.18118352,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.18417554,"top":0.575419,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.5937749,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.030585106,"top":0.5937749,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.62968874,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// Primary path: derive from policyName in response body","depth":27,"bounds":{"left":0.030585106,"top":0.62968874,"width":0.15658244,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.6480447,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"bounds":{"left":0.030585106,"top":0.6480447,"width":0.019614361,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.049867023,"top":0.6480447,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.05285904,"top":0.6480447,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.055518616,"top":0.6480447,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.05851064,"top":0.6480447,"width":0.013962766,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07247341,"top":0.6480447,"width":0.0056515955,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"bounds":{"left":0.077792555,"top":0.6480447,"width":0.03656915,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.1143617,"top":0.6480447,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.11702128,"top":0.6480447,"width":0.0056515955,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.12267287,"top":0.6480447,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.12533244,"top":0.6480447,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.6839585,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"bounds":{"left":0.030585106,"top":0.6839585,"width":0.016954787,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04720745,"top":0.6839585,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.049867023,"top":0.6839585,"width":0.014295213,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.06382979,"top":0.6839585,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"bounds":{"left":0.06948138,"top":0.6839585,"width":0.02825798,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.09740692,"top":0.6839585,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"match","depth":27,"bounds":{"left":0.100398935,"top":0.6839585,"width":0.013962766,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1143617,"top":0.6839585,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11702128,"top":0.6839585,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"bounds":{"left":0.11968085,"top":0.6839585,"width":0.019946808,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.1392952,"top":0.6839585,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14228724,"top":0.6839585,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.14494681,"top":0.6839585,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.70231444,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'SECONDLY'","depth":27,"bounds":{"left":0.04155585,"top":0.70231444,"width":0.02825798,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.06948138,"top":0.70231444,"width":0.03656915,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.70231444,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.70231444,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"bounds":{"left":0.1143617,"top":0.70231444,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.11702128,"top":0.70231444,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.7206704,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'TEN_SECONDLY_ROLLING'","depth":27,"bounds":{"left":0.04155585,"top":0.7206704,"width":0.061835106,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.10305851,"top":0.7206704,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.7206704,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.7206704,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"8","depth":27,"bounds":{"left":0.1143617,"top":0.7206704,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.11702128,"top":0.7206704,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.7390263,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'DAILY'","depth":27,"bounds":{"left":0.04155585,"top":0.7390263,"width":0.019946808,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.7390263,"width":0.04488032,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.7390263,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.7390263,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3600","depth":27,"bounds":{"left":0.1143617,"top":0.7390263,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.12533244,"top":0.7390263,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.7565842,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"default","depth":27,"bounds":{"left":0.04155585,"top":0.7565842,"width":0.019946808,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.7565842,"width":0.04488032,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.7565842,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.7565842,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":27,"bounds":{"left":0.1143617,"top":0.7565842,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.11702128,"top":0.7565842,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.77494013,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.030585106,"top":0.77494013,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.03324468,"top":0.77494013,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.035904255,"top":0.77494013,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.019281914,"top":0.7932961,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"bounds":{"left":0.019281914,"top":0.8292099,"width":0.019614361,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.038896278,"top":0.8292099,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"bounds":{"left":0.04155585,"top":0.8292099,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.06382979,"top":0.8292099,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"bounds":{"left":0.066821806,"top":0.8292099,"width":0.03656915,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.10305851,"top":0.8292099,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"bounds":{"left":0.10571808,"top":0.8292099,"width":0.025598405,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.13098404,"top":0.8292099,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.13364361,"top":0.8292099,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.1392952,"top":0.8292099,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.14228724,"top":0.8292099,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14494681,"top":0.8292099,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"bounds":{"left":0.14760639,"top":0.8292099,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"string","depth":27,"bounds":{"left":0.1505984,"top":0.8292099,"width":0.016954787,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.019281914,"top":0.8475658,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.8659218,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"bounds":{"left":0.030585106,"top":0.8659218,"width":0.013962766,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04454787,"top":0.8659218,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.055518616,"top":0.8659218,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.8659218,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.061170213,"top":0.8659218,"width":0.014295213,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07513298,"top":0.8659218,"width":0.0056515955,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractBody","depth":27,"bounds":{"left":0.080784574,"top":0.8659218,"width":0.030917553,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.111369684,"top":0.8659218,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.1143617,"top":0.8659218,"width":0.0056515955,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.11968085,"top":0.8659218,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.12267287,"top":0.8659218,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.88427776,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"bounds":{"left":0.030585106,"top":0.88427776,"width":0.022273935,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05285904,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.055518616,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"json_decode","depth":27,"bounds":{"left":0.061170213,"top":0.88427776,"width":0.030917553,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.091755316,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"bounds":{"left":0.09474734,"top":0.88427776,"width":0.013962766,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.1087101,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"true","depth":27,"bounds":{"left":0.1143617,"top":0.88427776,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.12533244,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.12832446,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"bounds":{"left":0.13098404,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.13364361,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.13663563,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"bounds":{"left":0.1392952,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"bounds":{"left":0.14228724,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.14494681,"top":0.88427776,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.9018356,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"bounds":{"left":0.030585106,"top":0.9018356,"width":0.016954787,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04720745,"top":0.9018356,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"strtoupper","depth":27,"bounds":{"left":0.049867023,"top":0.9018356,"width":0.02825798,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.077792555,"top":0.9018356,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"}]...
|
-465315953769404753
|
-7921867235185013732
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(...
|
4587
|
NULL
|
NULL
|
NULL
|
|
4590
|
168
|
0
|
2026-05-07T14:12:16.489040+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163136489_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous version","depth":21,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 / 2","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Next version","depth":21,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: That confirms the diagnosis.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: That confirms the diagnosis.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"That confirms the diagnosis. Look at what's there and — more importantly — what isn't:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Missing from the 429 response:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Present that's actually useful:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"✅","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(for debugging / filing tickets with HubSpot)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 (the","depth":25,"on_screen":false,"role_description":"text"}]...
|
3327376655221363504
|
-3310745814913033064
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4591
|
168
|
1
|
2026-05-07T14:12:19.416902+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163139416_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous version","depth":21,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 / 2","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Next version","depth":21,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: That confirms the diagnosis.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: That confirms the diagnosis.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"That confirms the diagnosis. Look at what's there and — more importantly — what isn't:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Missing from the 429 response:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Present that's actually useful:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"✅","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(for debugging / filing tickets with HubSpot)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 (the","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/contact/search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"one) — search responses always strip rate-limit headers, error or not.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What this means","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What this means","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter()","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"cannot rely on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from HubSpot.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The policy-based fallback is the actual primary path","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"bounds":{"left":0.22340426,"top":0.07741421,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"public","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// Opportunistic: HubSpot rarely sends Retry-After, but use it if present","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"extractHeaders","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headerValue","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'Retry-After'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is_numeric","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// Primary path: derive from policyName in response body","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.019952115,"width":0.011303191,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"bounds":{"left":0.030585106,"top":0.019952115,"width":0.019614361,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.049867023,"top":0.019952115,"width":0.0029920214,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.05285904,"top":0.019952115,"width":0.0029920214,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.055518616,"top":0.019952115,"width":0.0029920214,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.05851064,"top":0.019952115,"width":0.013962766,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07247341,"top":0.019952115,"width":0.0056515955,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"bounds":{"left":0.077792555,"top":0.019952115,"width":0.03656915,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.1143617,"top":0.019952115,"width":0.0029920214,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.11702128,"top":0.019952115,"width":0.0056515955,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.12267287,"top":0.019952115,"width":0.0029920214,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.12533244,"top":0.019952115,"width":0.0029920214,"height":0.0047885077},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.046288908,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"bounds":{"left":0.030585106,"top":0.046288908,"width":0.016954787,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.030585106,"top":0.04708699,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":5,"bounds":{"left":0.03357713,"top":0.04708699,"width":0.013962766,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04720745,"top":0.046288908,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.049867023,"top":0.046288908,"width":0.014295213,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.050199468,"top":0.04708699,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":4,"bounds":{"left":0.05285904,"top":0.04708699,"width":0.011303191,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.06382979,"top":0.046288908,"width":0.005984043,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.06416223,"top":0.04708699,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.066821806,"top":0.04708699,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"bounds":{"left":0.06948138,"top":0.046288908,"width":0.02825798,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.06981383,"top":0.04708699,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":9,"bounds":{"left":0.07247341,"top":0.04708699,"width":0.025265958,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.09740692,"top":0.046288908,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"match","depth":27,"bounds":{"left":0.100398935,"top":0.046288908,"width":0.013962766,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.100398935,"top":0.04708699,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":4,"bounds":{"left":0.103390954,"top":0.04708699,"width":0.011303191,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1143617,"top":0.046288908,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.11702128,"top":0.046288908,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"bounds":{"left":0.11968085,"top":0.046288908,"width":0.019946808,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1200133,"top":0.04708699,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.12267287,"top":0.04708699,"width":0.016954787,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.1392952,"top":0.046288908,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14228724,"top":0.046288908,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.14494681,"top":0.046288908,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.06464485,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'SECONDLY'","depth":27,"bounds":{"left":0.04155585,"top":0.06464485,"width":0.02825798,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.041888297,"top":0.065442935,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":9,"bounds":{"left":0.04454787,"top":0.065442935,"width":0.025265958,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.06948138,"top":0.06464485,"width":0.03656915,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.06464485,"width":0.005984043,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10605053,"top":0.065442935,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.1087101,"top":0.065442935,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.06464485,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"bounds":{"left":0.1143617,"top":0.06464485,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.11702128,"top":0.06464485,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.0830008,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'TEN_SECONDLY_ROLLING'","depth":27,"bounds":{"left":0.04155585,"top":0.0830008,"width":0.061835106,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.041888297,"top":0.0830008,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":21,"bounds":{"left":0.04454787,"top":0.0830008,"width":0.058843084,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.10305851,"top":0.0830008,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.0830008,"width":0.005984043,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10605053,"top":0.0830008,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.1087101,"top":0.0830008,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.0830008,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"8","depth":27,"bounds":{"left":0.1143617,"top":0.0830008,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.11702128,"top":0.0830008,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.101356745,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'DAILY'","depth":27,"bounds":{"left":0.04155585,"top":0.101356745,"width":0.019946808,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.041888297,"top":0.101356745,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.04454787,"top":0.101356745,"width":0.016954787,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.101356745,"width":0.04488032,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.101356745,"width":0.005984043,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10605053,"top":0.101356745,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.1087101,"top":0.101356745,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.101356745,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3600","depth":27,"bounds":{"left":0.1143617,"top":0.101356745,"width":0.011303191,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1143617,"top":0.101356745,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":3,"bounds":{"left":0.11735372,"top":0.101356745,"width":0.00831117,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.12533244,"top":0.101356745,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.118914604,"width":0.022606382,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"default","depth":27,"bounds":{"left":0.04155585,"top":0.118914604,"width":0.019946808,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.041888297,"top":0.11971269,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.04454787,"top":0.11971269,"width":0.016954787,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.061170213,"top":0.118914604,"width":0.04488032,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"bounds":{"left":0.10571808,"top":0.118914604,"width":0.005984043,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10605053,"top":0.11971269,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.1087101,"top":0.11971269,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.118914604,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":27,"bounds":{"left":0.1143617,"top":0.118914604,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.11702128,"top":0.118914604,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.13727055,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.030585106,"top":0.13727055,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.03324468,"top":0.13727055,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.035904255,"top":0.13727055,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"bounds":{"left":0.019281914,"top":0.15562649,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"bounds":{"left":0.019281914,"top":0.1915403,"width":0.019614361,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.019614361,"top":0.19233839,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":6,"bounds":{"left":0.022273935,"top":0.19233839,"width":0.016954787,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.038896278,"top":0.1915403,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"bounds":{"left":0.04155585,"top":0.1915403,"width":0.022606382,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.041888297,"top":0.19233839,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.04454787,"top":0.19233839,"width":0.019614361,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.06382979,"top":0.1915403,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"bounds":{"left":0.066821806,"top":0.1915403,"width":0.03656915,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.066821806,"top":0.19233839,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":12,"bounds":{"left":0.06981383,"top":0.19233839,"width":0.03357713,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.10305851,"top":0.1915403,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"bounds":{"left":0.10571808,"top":0.1915403,"width":0.025598405,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10605053,"top":0.19233839,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":8,"bounds":{"left":0.1087101,"top":0.19233839,"width":0.022606382,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.13098404,"top":0.1915403,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.13364361,"top":0.1915403,"width":0.005984043,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.13397606,"top":0.19233839,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.13663563,"top":0.19233839,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.1392952,"top":0.1915403,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.14228724,"top":0.1915403,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14494681,"top":0.1915403,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"bounds":{"left":0.14760639,"top":0.1915403,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"string","depth":27,"bounds":{"left":0.1505984,"top":0.1915403,"width":0.016954787,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1505984,"top":0.19233839,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":5,"bounds":{"left":0.15359043,"top":0.19233839,"width":0.013962766,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"bounds":{"left":0.019281914,"top":0.20989625,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.22825219,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"bounds":{"left":0.030585106,"top":0.22825219,"width":0.013962766,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.030585106,"top":0.22825219,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":4,"bounds":{"left":0.03357713,"top":0.22825219,"width":0.011303191,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04454787,"top":0.22825219,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.055518616,"top":0.22825219,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.22825219,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"bounds":{"left":0.061170213,"top":0.22825219,"width":0.014295213,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.061502658,"top":0.22825219,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":4,"bounds":{"left":0.06416223,"top":0.22825219,"width":0.011303191,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"bounds":{"left":0.07513298,"top":0.22825219,"width":0.0056515955,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.075465426,"top":0.22825219,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.078125,"top":0.22825219,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"extractBody","depth":27,"bounds":{"left":0.080784574,"top":0.22825219,"width":0.030917553,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.080784574,"top":0.22825219,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":10,"bounds":{"left":0.08377659,"top":0.22825219,"width":0.027925532,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.111369684,"top":0.22825219,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"bounds":{"left":0.1143617,"top":0.22825219,"width":0.0056515955,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1143617,"top":0.22825219,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.11735372,"top":0.22825219,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.11968085,"top":0.22825219,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.12267287,"top":0.22825219,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.24660814,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"bounds":{"left":0.030585106,"top":0.24660814,"width":0.022273935,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.030585106,"top":0.24660814,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.03357713,"top":0.24660814,"width":0.019614361,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05285904,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"bounds":{"left":0.055518616,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.05851064,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"json_decode","depth":27,"bounds":{"left":0.061170213,"top":0.24660814,"width":0.030917553,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.061502658,"top":0.24660814,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":10,"bounds":{"left":0.06416223,"top":0.24660814,"width":0.02825798,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.091755316,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"bounds":{"left":0.09474734,"top":0.24660814,"width":0.013962766,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.09474734,"top":0.24660814,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":4,"bounds":{"left":0.09773936,"top":0.24660814,"width":0.011303191,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"bounds":{"left":0.1087101,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.111369684,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"true","depth":27,"bounds":{"left":0.1143617,"top":0.24660814,"width":0.011303191,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.1143617,"top":0.24660814,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":3,"bounds":{"left":0.11735372,"top":0.24660814,"width":0.00831117,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"bounds":{"left":0.12533244,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.12832446,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"bounds":{"left":0.13098404,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"bounds":{"left":0.13364361,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.13663563,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"bounds":{"left":0.1392952,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"bounds":{"left":0.14228724,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"bounds":{"left":0.14494681,"top":0.24660814,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.019281914,"top":0.264166,"width":0.011303191,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"bounds":{"left":0.030585106,"top":0.264166,"width":0.016954787,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.030585106,"top":0.26496407,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":5,"bounds":{"left":0.03357713,"top":0.26496407,"width":0.013962766,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.04720745,"top":0.264166,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"strtoupper","depth":27,"bounds":{"left":0.049867023,"top":0.264166,"width":0.02825798,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.050199468,"top":0.26496407,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":9,"bounds":{"left":0.05285904,"top":0.26496407,"width":0.025265958,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"bounds":{"left":0.077792555,"top":0.264166,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"bounds":{"left":0.080784574,"top":0.264166,"width":0.022606382,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.080784574,"top":0.26496407,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":7,"bounds":{"left":0.08377659,"top":0.26496407,"width":0.019614361,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"bounds":{"left":0.10305851,"top":0.264166,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'policyName'","depth":27,"bounds":{"left":0.10571808,"top":0.264166,"width":0.033909574,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10605053,"top":0.26496407,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":11,"bounds":{"left":0.1087101,"top":0.26496407,"width":0.030917553,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"bounds":{"left":0.1392952,"top":0.264166,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.14228724,"top":0.264166,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"bounds":{"left":0.14494681,"top":0.264166,"width":0.0056515955,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.14527926,"top":0.26496407,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.14793883,"top":0.26496407,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"bounds":{"left":0.1505984,"top":0.264166,"width":0.0029920214,"height":0.014365523},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"''","depth":27,"bounds":{"left":0.15325798,"top":0.264166,"width":0.005984043,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.15359043,"top":0.26496407,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":1,"bounds":{"left":0.15625,"top":0.26496407,"width":0.0029920214,"height":0.014365523}}],"role_description":"text"}]...
|
-4841207262735101110
|
-7921867235185013732
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''...
|
4590
|
NULL
|
NULL
|
NULL
|
|
4592
|
167
|
0
|
2026-05-07T14:12:21.990268+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163141990_m1.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''
)
?
:
null
;
}
private
function
withJitter
(
int
$seconds
)
:
int
{
// ±20% randomization to prevent thundering-herd retries
$jitter
=
(
int
)
(
$seconds
*
0.2
)
;
return
max
(
1
,
$seconds
+
random_int
(
-
$jitter
,
$jitter
)
)
;
}
Why those defaults
Why those defaults
SECONDLY
→ 2 seconds.
The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.
TEN_SECONDLY_ROLLING
→ 8 seconds.
The window is 10 seconds
rolling
. If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.
DAILY
→ 3600 (1 hour) or push to next-day queue.
Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)
Jitter
is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.
Bonus: log the correlation ID
Bonus: log the correlation ID
You're throwing away a useful debugging signal. The
x-hubspot-correlation-id
from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support "why did this specific call get rate limited" or correlate with their internal logs if you ever need to escalate. Capture it:
Copy to clipboard
php
$correlationId
=
$this
->
headerValue
(
$headers
,
'x-hubspot-correlation-id'
)
??
(
$payload
[
'correlationId'
]
??
null
)
;
$this
->
log
->
warning
(
'[Hubspot] 429 received'
,
[
'policy'
=>
$policy
,
'retry_after'
=>
$delaySeconds
,
'hubspot_correlation_id'
=>
$correlationId
,
'endpoint'
=>
$endpointForLogging
,
// pass this in
]
)
;
When a customer reports "my sync was slow today" and you can correlate to a flurry of 429s with HubSpot correlation IDs, the diagnosis path is much shorter.
So the final picture
So the final picture
For this codebase, given what HubSpot actually sends:
Pre-acquire from Redis bucket before every call
— this is your primary defense, since post-hoc info is sparse.
On 429, read
policyName
from the body
— the only reliable signal of which bucket fired.
Apply policy-specific backoff with jitter
—
Retry-After
is nice-to-have but rare in practice.
On success, capture
X-HubSpot-RateLimit-*
headers via Guzzle middleware
to clamp the local counter — these
are
present on 200 responses (just confirmed in your earlier Postman screenshot showing
Max=110
).
The asymmetry is the key insight: HubSpot tells you a lot when things are fine, almost nothing when they're not. Your limiter has to be smart enough to work with that.
Copy
Give positive feedback
Give negative feedback
Retry
Scroll to bottom
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Settings
Press and hold to record
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous version","depth":21,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 / 2","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Next version","depth":21,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: That confirms the diagnosis.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: That confirms the diagnosis.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"That confirms the diagnosis. Look at what's there and — more importantly — what isn't:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Missing from the 429 response:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Present that's actually useful:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"✅","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(for debugging / filing tickets with HubSpot)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 (the","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/contact/search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"one) — search responses always strip rate-limit headers, error or not.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What this means","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What this means","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter()","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"cannot rely on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from HubSpot.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The policy-based fallback is the actual primary path","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"public","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// Opportunistic: HubSpot rarely sends Retry-After, but use it if present","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"extractHeaders","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headerValue","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'Retry-After'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is_numeric","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// Primary path: derive from policyName in response body","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"match","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'SECONDLY'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'TEN_SECONDLY_ROLLING'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"8","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'DAILY'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3600","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"default","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"string","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractBody","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"json_decode","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"true","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"strtoupper","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'policyName'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"''","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"null","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// ±20% randomization to prevent thundering-herd retries","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"*","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"0.2","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"random_int","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Why those defaults","depth":24,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Why those defaults","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"SECONDLY","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ 2 seconds.","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"TEN_SECONDLY_ROLLING","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ 8 seconds.","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The window is 10 seconds","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"rolling","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":". If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"DAILY","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"→ 3600 (1 hour) or push to next-day queue.","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"Bonus: log the correlation ID","depth":24,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Bonus: log the correlation ID","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"You're throwing away a useful debugging signal. The","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support \"why did this specific call get rate limited\" or correlate with their internal logs if you ever need to escalate. Capture it:","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$correlationId","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"headerValue","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'x-hubspot-correlation-id'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'correlationId'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"null","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"log","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"warning","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'[Hubspot] 429 received'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'policy'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'retry_after'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$delaySeconds","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'hubspot_correlation_id'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$correlationId","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"'endpoint'","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"$endpointForLogging","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"// pass this in","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"When a customer reports \"my sync was slow today\" and you can correlate to a flurry of 429s with HubSpot correlation IDs, the diagnosis path is much shorter.","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXHeading","text":"So the final picture","depth":24,"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"So the final picture","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"For this codebase, given what HubSpot actually sends:","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Pre-acquire from Redis bucket before every call","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"— this is your primary defense, since post-hoc info is sparse.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"On 429, read","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"policyName","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"from the body","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"— the only reliable signal of which bucket fired.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Apply policy-specific backoff with jitter","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"—","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"is nice-to-have but rare in practice.","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"On success, capture","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-*","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"headers via Guzzle middleware","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"to clamp the local counter — these","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"are","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"present on 200 responses (just confirmed in your earlier Postman screenshot showing","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Max=110","depth":27,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":").","depth":26,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"The asymmetry is the key insight: HubSpot tells you a lot when things are fine, almost nothing when they're not. Your limiter has to be smart enough to work with that.","depth":25,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give positive feedback","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Give negative feedback","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Retry","depth":22,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scroll to bottom","depth":21,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Write a message…","depth":25,"on_screen":true,"value":"Write a message…","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Write a message…","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Add files, connectors, and more","depth":27,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Model: Opus 4.7 Adaptive","depth":27,"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Opus 4.7","depth":29,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Adaptive","depth":28,"on_screen":true,"role_description":"text"},{"role":"AXPopUpButton","text":"Settings","depth":25,"bounds":{"left":0.0,"top":0.0,"width":0.019444445,"height":0.031111112},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Press and hold to record","depth":25,"bounds":{"left":0.0,"top":0.0,"width":0.022222223,"height":0.031111112},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":22,"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude is AI and can make mistakes. Please double-check responses.","depth":23,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"Preview","depth":20,"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"Code","depth":20,"on_screen":true,"role_description":"radio button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Hubspot rate limits reference · MD","depth":19,"on_screen":true,"help_text":"Hubspot rate limits reference","role_description":"heading"},{"role":"AXStaticText","text":"Hubspot rate limits reference","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"·","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MD","depth":20,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Copy","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Go back","depth":19,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"HubSpot Rate Limit Reference","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"HubSpot Rate Limit Reference","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Quick reference for checking what rate limits apply to a portal.","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Three buckets, three discovery methods","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Three buckets, three discovery methods","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Daily","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Response headers on any non-search call","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Not queryable","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/account-info/v3/api-usage/daily/private-apps","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto-discoverable per portal","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Fixed at 5 req/sec, only confirmable via 429","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Private apps only — OAuth has no daily limit","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bucket","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"How to check","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notes","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"1. Burst limit","depth":22,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"1. Burst limit","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Make any non-search call and read the","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"response headers","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(not body).","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":25,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"http","depth":24,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GET https://api.hubapi.com/account-info/v3/details","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Authorization","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Bearer {portal_access_token}","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Headers in response:","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Burst ceiling","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calls left in current window","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Window size, always","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"10000","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Deprecated, ignore","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Header","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Secondly-Remaining","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Meaning","depth":25,"on_screen":false,"role_description":"text"}]...
|
5467167207979682329
|
-7905137616557880166
|
idle
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''
)
?
:
null
;
}
private
function
withJitter
(
int
$seconds
)
:
int
{
// ±20% randomization to prevent thundering-herd retries
$jitter
=
(
int
)
(
$seconds
*
0.2
)
;
return
max
(
1
,
$seconds
+
random_int
(
-
$jitter
,
$jitter
)
)
;
}
Why those defaults
Why those defaults
SECONDLY
→ 2 seconds.
The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.
TEN_SECONDLY_ROLLING
→ 8 seconds.
The window is 10 seconds
rolling
. If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.
DAILY
→ 3600 (1 hour) or push to next-day queue.
Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)
Jitter
is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.
Bonus: log the correlation ID
Bonus: log the correlation ID
You're throwing away a useful debugging signal. The
x-hubspot-correlation-id
from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support "why did this specific call get rate limited" or correlate with their internal logs if you ever need to escalate. Capture it:
Copy to clipboard
php
$correlationId
=
$this
->
headerValue
(
$headers
,
'x-hubspot-correlation-id'
)
??
(
$payload
[
'correlationId'
]
??
null
)
;
$this
->
log
->
warning
(
'[Hubspot] 429 received'
,
[
'policy'
=>
$policy
,
'retry_after'
=>
$delaySeconds
,
'hubspot_correlation_id'
=>
$correlationId
,
'endpoint'
=>
$endpointForLogging
,
// pass this in
]
)
;
When a customer reports "my sync was slow today" and you can correlate to a flurry of 429s with HubSpot correlation IDs, the diagnosis path is much shorter.
So the final picture
So the final picture
For this codebase, given what HubSpot actually sends:
Pre-acquire from Redis bucket before every call
— this is your primary defense, since post-hoc info is sparse.
On 429, read
policyName
from the body
— the only reliable signal of which bucket fired.
Apply policy-specific backoff with jitter
—
Retry-After
is nice-to-have but rare in practice.
On success, capture
X-HubSpot-RateLimit-*
headers via Guzzle middleware
to clamp the local counter — these
are
present on 200 responses (just confirmed in your earlier Postman screenshot showing
Max=110
).
The asymmetry is the key insight: HubSpot tells you a lot when things are fine, almost nothing when they're not. Your limiter has to be smart enough to work with that.
Copy
Give positive feedback
Give negative feedback
Retry
Scroll to bottom
Write a message…
Write a message…
Add files, connectors, and more
Model: Opus 4.7 Adaptive
Opus 4.7
Adaptive
Settings
Press and hold to record
Claude is AI and can make mistakes. Please double-check responses.
Claude is AI and can make mistakes. Please double-check responses.
Preview
Code
Hubspot rate limits reference · MD
Hubspot rate limits reference
·
MD
Copy
Go back
HubSpot Rate Limit Reference
HubSpot Rate Limit Reference
Quick reference for checking what rate limits apply to a portal.
Three buckets, three discovery methods
Three buckets, three discovery methods
Bucket
How to check
Notes
Burst
Response headers on any non-search call
Auto-discoverable per portal
Search
Not queryable
Fixed at 5 req/sec, only confirmable via 429
Daily
/account-info/v3/api-usage/daily/private-apps
Private apps only — OAuth has no daily limit
Bucket
Burst
Search
Daily
How to check
Response headers on any non-search call
Not queryable
/account-info/v3/api-usage/daily/private-apps
Notes
Auto-discoverable per portal
Fixed at 5 req/sec, only confirmable via 429
Private apps only — OAuth has no daily limit
Bucket
How to check
Notes
1. Burst limit
1. Burst limit
Make any non-search call and read the
response headers
(not body).
Copy to clipboard
http
GET https://api.hubapi.com/account-info/v3/details
Authorization
:
Bearer {portal_access_token}
Headers in response:
Header
Meaning
X-HubSpot-RateLimit-Max
Burst ceiling
X-HubSpot-RateLimit-Remaining
Calls left in current window
X-HubSpot-RateLimit-Interval-Milliseconds
Window size, always
10000
X-HubSpot-RateLimit-Secondly
Deprecated, ignore
X-HubSpot-RateLimit-Secondly-Remaining
Deprecated, ignore
Header
X-HubSpot-RateLimit-Max
X-HubSpot-RateLimit-Remaining
X-HubSpot-RateLimit-Interval-Milliseconds
X-HubSpot-RateLimit-Secondly
X-HubSpot-RateLimit-Secondly-Remaining
Meaning...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4593
|
168
|
2
|
2026-05-07T14:12:34.456680+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163154456_m2.jpg...
|
Claude
|
Claude
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''
)
?
:
null
;
}
private
function
withJitter
(
int
$seconds
)
:
int
{
// ±20% randomization to prevent thundering-herd retries
$jitter
=
(
int
)
(
$seconds
*
0.2
)
;
return
max
(
1
,
$seconds
+
random_int
(
-
$jitter
,
$jitter
)
)
;
}
Why those defaults
Why those defaults
SECONDLY
→ 2 seconds.
The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.
TEN_SECONDLY_ROLLING
→ 8 seconds.
The window is 10 seconds
rolling
. If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.
DAILY
→ 3600 (1 hour) or push to next-day queue.
Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)
Jitter
is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.
Bonus: log the correlation ID
Bonus: log the correlation ID
You're throwing away a useful debugging signal. The
x-hubspot-correlation-id
from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support "why did this specific call get rate limited" or correlate with their internal logs if you ever need to escalate. Capture it:
Copy to clipboard
php
$correlationId...
|
[{"role":"AXLink","text":& [{"role":"AXLink","text":"Skip to content","depth":14,"bounds":{"left":0.029587766,"top":0.03830806,"width":0.0003324468,"height":0.0007980846},"on_screen":true,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Skip to content","depth":15,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Click to collapse","depth":16,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.030585106,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.06703911,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":16,"bounds":{"left":0.10538564,"top":0.06703911,"width":0.027925532,"height":0.011971269}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘B","depth":16,"bounds":{"left":0.1349734,"top":0.06703911,"width":0.0063164895,"height":0.011971269},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Drag to resize","depth":16,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.025930852,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.10239362,"top":0.079010375,"width":0.0029920214,"height":0.011971269}},{"char_start":1,"char_count":13,"bounds":{"left":0.10538564,"top":0.079010375,"width":0.022938829,"height":0.011971269}}],"role_description":"text"},{"role":"AXButton","text":"Open sidebar","depth":14,"bounds":{"left":0.029920213,"top":0.02793296,"width":0.00930851,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat","depth":16,"bounds":{"left":0.004986702,"top":0.059856344,"width":0.025930852,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cowork","depth":16,"bounds":{"left":0.03158245,"top":0.059856344,"width":0.03125,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code","depth":16,"bounds":{"left":0.0631649,"top":0.059856344,"width":0.026928192,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New chat ⌘N","depth":15,"bounds":{"left":0.0043218085,"top":0.08938547,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"New chat","depth":16,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.018949468,"height":0.012769354},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.014295213,"top":0.0933759,"width":0.003656915,"height":0.013567438}},{"char_start":1,"char_count":7,"bounds":{"left":0.01761968,"top":0.0933759,"width":0.015957447,"height":0.013567438}}],"role_description":"text"},{"role":"AXStaticText","text":"⌘N","depth":17,"bounds":{"left":0.08178192,"top":0.0933759,"width":0.006981383,"height":0.012769354},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Projects","depth":15,"bounds":{"left":0.0043218085,"top":0.110135674,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Artifacts","depth":15,"bounds":{"left":0.0043218085,"top":0.1300878,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Customize","depth":15,"bounds":{"left":0.0043218085,"top":0.15003991,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pinned","depth":16,"bounds":{"left":0.0063164895,"top":0.18914606,"width":0.08377659,"height":0.013567438},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"Bulgarian citizenship application process for EU residents","depth":18,"bounds":{"left":0.0043218085,"top":0.20590582,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Bulgarian citizenship application process for EU residents","depth":19,"bounds":{"left":0.08344415,"top":0.20909816,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Dawarich location tracking project","depth":18,"bounds":{"left":0.0043218085,"top":0.22745411,"width":0.08643617,"height":0.019952115},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Dawarich location tracking project","depth":19,"bounds":{"left":0.08344415,"top":0.22984837,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Recents","depth":16,"bounds":{"left":0.0063164895,"top":0.25698325,"width":0.06349734,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":true},{"role":"AXButton","text":"View all","depth":16,"bounds":{"left":0.07114362,"top":0.25698325,"width":0.018949468,"height":0.012769354},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy","depth":18,"bounds":{"left":0.0043218085,"top":0.27294493,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.08344415,"top":0.27613726,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe retention policy code location","depth":18,"bounds":{"left":0.0043218085,"top":0.29449323,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe retention policy code location","depth":19,"bounds":{"left":0.08344415,"top":0.29768556,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Viewing retention policy in screenpipe","depth":18,"bounds":{"left":0.0043218085,"top":0.31524342,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Viewing retention policy in screenpipe","depth":19,"bounds":{"left":0.08344415,"top":0.31843576,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Clean shot x video recording termination issue","depth":18,"bounds":{"left":0.0043218085,"top":0.3367917,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Clean shot x video recording termination issue","depth":19,"bounds":{"left":0.08344415,"top":0.33998403,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit handling with executeRequest","depth":18,"bounds":{"left":0.0043218085,"top":0.3575419,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit handling with executeRequest","depth":19,"bounds":{"left":0.08344415,"top":0.36073422,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Untitled","depth":18,"bounds":{"left":0.0043218085,"top":0.3790902,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options","depth":19,"bounds":{"left":0.08344415,"top":0.38228253,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 Screen pipe. Is there ability…","depth":18,"bounds":{"left":0.0043218085,"top":0.39984038,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 Screen pipe. Is there ability…","depth":19,"bounds":{"left":0.08344415,"top":0.40303272,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"SMB mount access inconsistency between Finder and iTerm","depth":18,"bounds":{"left":0.0043218085,"top":0.42138866,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for SMB mount access inconsistency between Finder and iTerm","depth":19,"bounds":{"left":0.08344415,"top":0.4237829,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"💬 What is the best switch I can…","depth":18,"bounds":{"left":0.0043218085,"top":0.44213888,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for 💬 What is the best switch I can…","depth":19,"bounds":{"left":0.08344415,"top":0.44533122,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Permission denied on screenpipe volume","depth":18,"bounds":{"left":0.0043218085,"top":0.46288908,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Permission denied on screenpipe volume","depth":19,"bounds":{"left":0.08344415,"top":0.4660814,"width":0.005984043,"height":0.015163607},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Screenpipe sync database attachment error","depth":18,"bounds":{"left":0.0043218085,"top":0.48443735,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Screenpipe sync database attachment error","depth":19,"bounds":{"left":0.08344415,"top":0.48762968,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Last swimming outing with Dani","depth":18,"bounds":{"left":0.0043218085,"top":0.5051876,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Last swimming outing with Dani","depth":19,"bounds":{"left":0.08344415,"top":0.5083799,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Definition of incarcerated","depth":18,"bounds":{"left":0.0043218085,"top":0.52673584,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Definition of incarcerated","depth":19,"bounds":{"left":0.08344415,"top":0.52992815,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chromecast remote volume buttons not working","depth":18,"bounds":{"left":0.0043218085,"top":0.547486,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Chromecast remote volume buttons not working","depth":19,"bounds":{"left":0.08344415,"top":0.5506784,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Salesforce API errors with Organization and FieldDefinition queries","depth":18,"bounds":{"left":0.0043218085,"top":0.56903434,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Salesforce API errors with Organization and FieldDefinition queries","depth":19,"bounds":{"left":0.08344415,"top":0.57222664,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Daily activity summary from screenpipe data","depth":18,"bounds":{"left":0.0043218085,"top":0.5897845,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Daily activity summary from screenpipe data","depth":19,"bounds":{"left":0.08344415,"top":0.59297687,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"MacBook unexpected restarts and kanji screen","depth":18,"bounds":{"left":0.0043218085,"top":0.6113328,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for MacBook unexpected restarts and kanji screen","depth":19,"bounds":{"left":0.08344415,"top":0.61452514,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Security patch review and testing guidance","depth":18,"bounds":{"left":0.0043218085,"top":0.632083,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Security patch review and testing guidance","depth":19,"bounds":{"left":0.08344415,"top":0.63527536,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Food calorie values reference","depth":18,"bounds":{"left":0.0043218085,"top":0.65363127,"width":0.08643617,"height":0.0207502},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Food calorie values reference","depth":19,"bounds":{"left":0.08344415,"top":0.65682364,"width":0.005984043,"height":0.014365523},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tracking location history from last week","depth":18,"bounds":{"left":0.0043218085,"top":0.6743815,"width":0.08643617,"height":0.011173184},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"More options for Tracking location history from last week","depth":19,"bounds":{"left":0.08344415,"top":0.6775738,"width":0.005984043,"height":0.007980846},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Lukas Pro","depth":15,"bounds":{"left":0.0043218085,"top":0.6943336,"width":0.037898935,"height":0.01915403},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Get apps and extensions","depth":15,"bounds":{"left":0.08277926,"top":0.6943336,"width":0.007978723,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"HubSpot rate limit implementation strategy, rename chat","depth":19,"bounds":{"left":0.043218084,"top":0.02793296,"width":0.09773936,"height":0.022346368},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"HubSpot rate limit implementation strategy","depth":21,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.09507979,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.04454787,"top":0.031923383,"width":0.003656915,"height":0.014365523}},{"char_start":1,"char_count":41,"bounds":{"left":0.048204787,"top":0.031923383,"width":0.09142287,"height":0.014365523}}],"role_description":"text"},{"role":"AXPopUpButton","text":"More options for HubSpot rate limit implementation strategy","depth":19,"bounds":{"left":0.14128989,"top":0.02793296,"width":0.0066489363,"height":0.022346368},"on_screen":true,"role_description":"pop-up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Share chat","depth":21,"bounds":{"left":0.234375,"top":0.026336791,"width":0.010638298,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Claude finished the response","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {\"headers\":{\"Date\":[\"Thu, 07 May 2026 14:08:35 GMT\"],\"Content-Type\":[\"application/json;charset=utf-8\"],\"Content-Length\":[\"227\"],\"Connection\":[\"keep-alive\"],\"CF-Ray\":[\"9f80cc29fd66dc1a-SOF\"],\"CF-Cache-Status\":[\"DYNAMIC\"],\"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\"Vary\":[\"origin\"],\"access-control-allow-credentials\":[\"false\"],\"server-timing\":[\"hcid;desc=\\\"019e02c4-d684-7f5d-be16-3d129d00c0a3\\\", cfr;desc=\\\"9f80cc2a06d23402-IAD\\\"\"],\"x-content-type-options\":[\"nosniff\"],\"x-hubspot-correlation-id\":[\"019e02c4-d684-7f5d-be16-3d129d00c0a3\"],\"Set-Cookie\":[\"__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-1.0.1.1-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\"Report-To\":[\"{\\\"endpoints\\\":[{\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\\\"}],\\\"group\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"NEL\":[\"{\\\"success_fraction\\\":0.01,\\\"report_to\\\":\\\"cf-nel\\\",\\\"max_age\\\":604800}\"],\"Server\":[\"cloudflare\"]}} {\"correlation_id\":\"d326aafa-339a-42c1-bf53-05ebd7f52782\",\"trace_id\":\"19b29b52-6663-4853-8096-e32bf6618df7\"}","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Show more","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"17:10","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Retry","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Copy","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Previous version","depth":21,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2 / 2","depth":21,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Next version","depth":21,"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXHeading","text":"Claude responded: That confirms the diagnosis.","depth":21,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Claude responded: That confirms the diagnosis.","depth":22,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":22,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Confirmed absence of retry headers; policy-based fallback necessary","depth":23,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"That confirms the diagnosis. Look at what's there and — more importantly — what isn't:","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Missing from the 429 response:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Remaining","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"❌","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"X-HubSpot-RateLimit-Interval-Milliseconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Present that's actually useful:","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"✅","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(for debugging / filing tickets with HubSpot)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"429 (the","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/contact/search","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"one) — search responses always strip rate-limit headers, error or not.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"What this means","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"What this means","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Your","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter()","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"cannot rely on","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Retry-After","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"from HubSpot.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The policy-based fallback is the actual primary path","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":", not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"public","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// Opportunistic: HubSpot rarely sends Retry-After, but use it if present","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"extractHeaders","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"headerValue","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$headers","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'Retry-After'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"if","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is_numeric","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$retryAfter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// Primary path: derive from policyName in response body","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"match","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$policy","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'SECONDLY'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"2","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'TEN_SECONDLY_ROLLING'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"8","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'DAILY'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"3600","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"default","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=>","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"extractPolicy","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Throwable","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"string","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$this","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"->","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"extractBody","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$e","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"json_decode","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$body","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"true","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"strtoupper","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$payload","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"[","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"'policyName'","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"]","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"??","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"''","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"?","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"null","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"private","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"function","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"withJitter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":":","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"{","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"// ±20% randomization to prevent thundering-herd retries","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"=","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"*","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"0.2","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"return","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"max","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$seconds","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"+","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"random_int","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"(","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"-","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":",","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"$jitter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":")","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":";","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"}","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Why those defaults","depth":24,"on_screen":false,"role_description":"heading"},{"role":"AXStaticText","text":"Why those defaults","depth":25,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SECONDLY","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"→ 2 seconds.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TEN_SECONDLY_ROLLING","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"→ 8 seconds.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"The window is 10 seconds","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"rolling","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":". If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DAILY","depth":28,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"→ 3600 (1 hour) or push to next-day queue.","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Jitter","depth":27,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.","depth":26,"on_screen":false,"role_description":"text"},{"role":"AXHeading","text":"Bonus: log the correlation ID","depth":24,"bounds":{"left":0.01462766,"top":0.019952115,"width":0.22240691,"height":0.01915403},"on_screen":true,"role_description":"heading"},{"role":"AXStaticText","text":"Bonus: log the correlation ID","depth":25,"bounds":{"left":0.017287234,"top":0.019952115,"width":0.08111702,"height":0.017557861},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.01915403,"width":0.004654255,"height":0.018355945}},{"char_start":1,"char_count":28,"bounds":{"left":0.021941489,"top":0.01915403,"width":0.07646277,"height":0.018355945}}],"role_description":"text"},{"role":"AXStaticText","text":"You're throwing away a useful debugging signal. The","depth":25,"bounds":{"left":0.017287234,"top":0.04708699,"width":0.1299867,"height":0.015961692},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.04708699,"width":0.0033244682,"height":0.016759777}},{"char_start":1,"char_count":50,"bounds":{"left":0.020611702,"top":0.04708699,"width":0.12566489,"height":0.016759777}}],"role_description":"text"},{"role":"AXStaticText","text":"x-hubspot-correlation-id","depth":26,"bounds":{"left":0.14860372,"top":0.047885075,"width":0.069148935,"height":0.015163607},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.14893617,"top":0.04868316,"width":0.0029920214,"height":0.014365523}},{"char_start":1,"char_count":23,"bounds":{"left":0.15159574,"top":0.04868316,"width":0.06648936,"height":0.014365523}}],"role_description":"text"},{"role":"AXStaticText","text":"from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support \"why did this specific call get rate limited\" or correlate with their internal logs if you ever need to escalate. Capture it:","depth":25,"bounds":{"left":0.017287234,"top":0.06624102,"width":0.20711437,"height":0.054269753},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.017287234,"top":0.06624102,"width":0.0019946808,"height":0.016759777}},{"char_start":1,"char_count":230,"bounds":{"left":0.017287234,"top":0.06624102,"width":0.20711437,"height":0.055067837}}],"role_description":"text"},{"role":"AXButton","text":"Copy to clipboard","depth":27,"bounds":{"left":0.22340426,"top":0.13806863,"width":0.010638298,"height":0.026336791},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"php","depth":26,"bounds":{"left":0.019281914,"top":0.14365523,"width":0.0076462766,"height":0.011971269},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.019614361,"top":0.14365523,"width":0.0023271276,"height":0.012769354}},{"char_start":1,"char_count":2,"bounds":{"left":0.021941489,"top":0.14365523,"width":0.004986702,"height":0.012769354}}],"role_description":"text"},{"role":"AXStaticText","text":"$correlationId","depth":27,"bounds":{"left":0.019281914,"top":0.16919394,"width":0.039228722,"height":0.014365523},"on_screen":true,"lines":[{"char_start":0,"char_count":1,"bounds":{"left":0.019614361,"top":0.16999201,"width":0.0026595744,"height":0.014365523}},{"char_start":1,"char_count":13,"bounds":{"left":0.022273935,"top":0.16999201,"width":0.036236703,"height":0.014365523}}],"role_description":"text"}]...
|
6175725832082475425
|
-3311448953664483696
|
visual_change
|
accessibility
|
NULL
|
Skip to content
Skip to content
Click to collapse
Skip to content
Skip to content
Click to collapse
⌘B
Drag to resize
Open sidebar
Chat
Cowork
Code
New chat ⌘N
New chat
⌘N
Projects
Artifacts
Customize
Pinned
Bulgarian citizenship application process for EU residents
More options for Bulgarian citizenship application process for EU residents
Dawarich location tracking project
More options for Dawarich location tracking project
Recents
View all
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Screenpipe retention policy code location
More options for Screenpipe retention policy code location
Viewing retention policy in screenpipe
More options for Viewing retention policy in screenpipe
Clean shot x video recording termination issue
More options for Clean shot x video recording termination issue
HubSpot rate limit handling with executeRequest
More options for HubSpot rate limit handling with executeRequest
Untitled
More options
💬 Screen pipe. Is there ability…
More options for 💬 Screen pipe. Is there ability…
SMB mount access inconsistency between Finder and iTerm
More options for SMB mount access inconsistency between Finder and iTerm
💬 What is the best switch I can…
More options for 💬 What is the best switch I can…
Permission denied on screenpipe volume
More options for Permission denied on screenpipe volume
Screenpipe sync database attachment error
More options for Screenpipe sync database attachment error
Last swimming outing with Dani
More options for Last swimming outing with Dani
Definition of incarcerated
More options for Definition of incarcerated
Chromecast remote volume buttons not working
More options for Chromecast remote volume buttons not working
Salesforce API errors with Organization and FieldDefinition queries
More options for Salesforce API errors with Organization and FieldDefinition queries
Daily activity summary from screenpipe data
More options for Daily activity summary from screenpipe data
MacBook unexpected restarts and kanji screen
More options for MacBook unexpected restarts and kanji screen
Security patch review and testing guidance
More options for Security patch review and testing guidance
Food calorie values reference
More options for Food calorie values reference
Tracking location history from last week
More options for Tracking location history from last week
Lukas Pro
Get apps and extensions
HubSpot rate limit implementation strategy, rename chat
HubSpot rate limit implementation strategy
More options for HubSpot rate limit implementation strategy
Share chat
Claude finished the response
[2026-05-07 14:08:35] local.INFO: [Hubspot] DEBUG Getting headers {"headers":{"Date":["Thu, 07 May 2026 14:08:35 GMT"],"Content-Type":["application/json;charset=utf-8"],"Content-Length":["227"],"Connection":["keep-alive"],"CF-Ray":["9f80cc29fd66dc1a-SOF"],"CF-Cache-Status":["DYNAMIC"],"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],"Vary":["origin"],"access-control-allow-credentials":["false"],"server-timing":["hcid;desc=\"019e02c4-d684-7f5d-be16-3d129d00c0a3\", cfr;desc=\"9f80cc2a06d23402-IAD\""],"x-content-type-options":["nosniff"],"x-hubspot-correlation-id":["019e02c4-d684-7f5d-be16-3d129d00c0a3"],"Set-Cookie":["__cf_bm=0LMCTstVmP5pyIYfLZ1Vgebevm4BnI_hv250XvZJDZI-1778162915-[IP_ADDRESS]-VEXAxU7TqgaJhUSJaNRDF5y59Eo7wOLd78Bm1nV_hNpBHZQPCrg.WMJL_LJy2qoR84rCgLXAlUPz4jkFWtD3fDjrRxypSlr4zAPvuPPaoNM; path=/; expires=Thu, 07-May-26 14:38:35 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Report-To":["{\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=4wbdllfukF%2BA1EOwyTeqZCIqvY2KcW%2BCX5f%2BaZsnWkxc6ccKufGimFrCvj%2FnyWTDpsdCMXE0ngrpMA7WjiyvJDXtlDtN2YAhtK127%2FDtKe1JCyjgKsMQXBSMhBDyTC7c\"}],\"group\":\"cf-nel\",\"max_age\":604800}"],"NEL":["{\"success_fraction\":0.01,\"report_to\":\"cf-nel\",\"max_age\":604800}"],"Server":["cloudflare"]}} {"correlation_id":"d326aafa-339a-42c1-bf53-05ebd7f52782","trace_id":"19b29b52-6663-4853-8096-e32bf6618df7"}
Show more
17:10
Retry
Edit
Copy
Previous version
2 / 2
Next version
Claude responded: That confirms the diagnosis.
Claude responded: That confirms the diagnosis.
Confirmed absence of retry headers; policy-based fallback necessary
Confirmed absence of retry headers; policy-based fallback necessary
That confirms the diagnosis. Look at what's there and — more importantly — what isn't:
Missing from the 429 response:
❌
Retry-After
❌
X-HubSpot-RateLimit-Max
❌
X-HubSpot-RateLimit-Remaining
❌
X-HubSpot-RateLimit-Interval-Milliseconds
Present that's actually useful:
✅
x-hubspot-correlation-id
(for debugging / filing tickets with HubSpot)
So HubSpot is giving you essentially no telemetry on this 429. No retry hint, no remaining-budget signal. The fact that even the regular rate-limit headers are absent strongly suggests this was the
search
429 (the
/contact/search
one) — search responses always strip rate-limit headers, error or not.
What this means
What this means
Your
parseRetryAfter()
cannot rely on
Retry-After
from HubSpot.
The policy-based fallback is the actual primary path
, not a fallback. The header lookup is dead code that's there in case HubSpot ever changes — fine to keep, but understand it'll basically never fire.
So flip the framing of your function: the policy-based logic is the main behavior, header lookup is opportunistic.
Copy to clipboard
php
public
function
parseRetryAfter
(
Throwable
$e
)
:
int
{
// Opportunistic: HubSpot rarely sends Retry-After, but use it if present
$headers
=
$this
->
extractHeaders
(
$e
)
;
$retryAfter
=
$this
->
headerValue
(
$headers
,
'Retry-After'
)
;
if
(
is_numeric
(
$retryAfter
)
)
{
return
$this
->
withJitter
(
max
(
1
,
(
int
)
$retryAfter
)
)
;
}
// Primary path: derive from policyName in response body
$policy
=
$this
->
extractPolicy
(
$e
)
;
return
$this
->
withJitter
(
match
(
$policy
)
{
'SECONDLY'
=>
2
,
'TEN_SECONDLY_ROLLING'
=>
8
,
'DAILY'
=>
3600
,
default
=>
5
,
}
)
;
}
private
function
extractPolicy
(
Throwable
$e
)
:
?
string
{
$body
=
$this
->
extractBody
(
$e
)
;
$payload
=
json_decode
(
$body
,
true
)
?
:
[
]
;
return
strtoupper
(
$payload
[
'policyName'
]
??
''
)
?
:
null
;
}
private
function
withJitter
(
int
$seconds
)
:
int
{
// ±20% randomization to prevent thundering-herd retries
$jitter
=
(
int
)
(
$seconds
*
0.2
)
;
return
max
(
1
,
$seconds
+
random_int
(
-
$jitter
,
$jitter
)
)
;
}
Why those defaults
Why those defaults
SECONDLY
→ 2 seconds.
The window is 1 second, so 1.x seconds is enough in theory. 2 with jitter keeps you safely past the boundary even with clock skew.
TEN_SECONDLY_ROLLING
→ 8 seconds.
The window is 10 seconds
rolling
. If you only just barely tipped over (which is the common case if your pre-acquire is roughly correct), the oldest call exits the window in under 10 seconds. 8 + jitter is a reasonable expected value; on the unlucky end you'll still 429 once and back off again.
DAILY
→ 3600 (1 hour) or push to next-day queue.
Don't keep retrying every 10 seconds for the rest of the day — that just burns through your 5% error budget. Better to dead-letter the job until tomorrow morning. (Less relevant for you since OAuth has no daily, but worth handling for completeness.)
Jitter
is non-negotiable. Without it, all your workers that hit the same 429 retry in lockstep and re-create the same 429 wave.
Bonus: log the correlation ID
Bonus: log the correlation ID
You're throwing away a useful debugging signal. The
x-hubspot-correlation-id
from the response (and there's also one in HubSpot's response body for 429s) lets you ask HubSpot support "why did this specific call get rate limited" or correlate with their internal logs if you ever need to escalate. Capture it:
Copy to clipboard
php
$correlationId...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4594
|
168
|
3
|
2026-05-07T14:12:37.464788+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163157464_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate wititheir internal logs if you ever need to escalate. Capture it:scorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', [pol1cv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter — these are present on 200 responses (just confirmedin your earlier Postman screenshot showing Max=110 ).The asy mmetry is the key insight: HubSpot tells you a lot when things are fine,almost nothing when they're not. Your limiter has to be smart enough to work with*x Hubspot• SearchYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissions to see who can edit, or upgrade to restore collaboration.POST search de • POST Readabal • POST Read a bat •GET ReadGET ReadGET read call•GET Get Engage•teration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileSummaryTotal requests sent ©Requests/second ©Avg. response time ©6.550109.04155 ms% 100P90 ©173 msp95 O196 ms"Lukas sterka 121• In 1omGET Read CopyGET https://api.tIteration run HSPOST search contaPOST search conta© Iteration run Se100% L2Thu 7 May 17:12:37UparadeIteration run SeNo environmentWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotv teration run HsGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engagemente old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesystem may not be able to generate the loadneeded for this test and the cest is likely toa Connect Git = Concole 5.l TermP99 ©287 msError % ©0,00Failure % ©0,00Peak CPU % ©100.0 %Peak Memory % ©195 %Filter bv requestsvAvg. response262 ms 140 req/s25017•08.25|17-08-21Dorformonso dotolle fortotol durotinnPOST search contact by email Copy17•08•2717•08•4617•08•496,550108.480.0017:08:55Falture %0.0017•09•01Resp. time (Ava. ms)17:09:0717.09-1217.09-19- Requests/second - Ava. response - Error % - Virtual users ..• CPU % ..• Memory %Min (ms)Max (ms)693287Globals Vault Tools s 000...
|
NULL
|
-4380527950069708648
|
NULL
|
visual_change
|
ocr
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate wititheir internal logs if you ever need to escalate. Capture it:scorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', [pol1cv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter — these are present on 200 responses (just confirmedin your earlier Postman screenshot showing Max=110 ).The asy mmetry is the key insight: HubSpot tells you a lot when things are fine,almost nothing when they're not. Your limiter has to be smart enough to work with*x Hubspot• SearchYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissions to see who can edit, or upgrade to restore collaboration.POST search de • POST Readabal • POST Read a bat •GET ReadGET ReadGET read call•GET Get Engage•teration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileSummaryTotal requests sent ©Requests/second ©Avg. response time ©6.550109.04155 ms% 100P90 ©173 msp95 O196 ms"Lukas sterka 121• In 1omGET Read CopyGET https://api.tIteration run HSPOST search contaPOST search conta© Iteration run Se100% L2Thu 7 May 17:12:37UparadeIteration run SeNo environmentWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotv teration run HsGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engagemente old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesystem may not be able to generate the loadneeded for this test and the cest is likely toa Connect Git = Concole 5.l TermP99 ©287 msError % ©0,00Failure % ©0,00Peak CPU % ©100.0 %Peak Memory % ©195 %Filter bv requestsvAvg. response262 ms 140 req/s25017•08.25|17-08-21Dorformonso dotolle fortotol durotinnPOST search contact by email Copy17•08•2717•08•4617•08•496,550108.480.0017:08:55Falture %0.0017•09•01Resp. time (Ava. ms)17:09:0717.09-1217.09-19- Requests/second - Ava. response - Error % - Virtual users ..• CPU % ..• Memory %Min (ms)Max (ms)693287Globals Vault Tools s 000...
|
4593
|
NULL
|
NULL
|
NULL
|
|
4595
|
168
|
4
|
2026-05-07T14:12:40.464148+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163160464_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate wititheir internal logs if you ever need to escalate. Capture it:scorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', ["policv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter - these are present on 200 responses (just confirmedin your earlier Postman screnshot showing Max=110).The asvmmetry is the kev insight: HubSpot tells vou a lot when things are fine.almost nothing when they're not. Your limiter has to be smart enough to work withunan*x Hubspot• SearchYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissions to see who can edit, or upgrade to restore collaboration.POST search de • POST Readabal • POST Reada bat eGET ReadGET ReadGET read call•GET Get Engage•teration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileSummaryTotal requests sent ©Requests/second ©Avg. response time ©6.550109.04155 ms% 100P90 ©173 msp95 O196 ms"Lukas sterka 121• In 1omGET Read CopyGET https://api.tIteration run HSPOST search contaPOST search conta© Iteration run Se100% L2Thu 7 May 17:12:40UparadeIteration run SeNo environmentWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotlteration run HSGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engagemente old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesystem may not be able to generate the loadneeded for this test and the cest is likely to§ Connect Git E Console E TerrP99 ©287 msError % ©0,00Failure % ©0,00Peak CPU % ©100.0 %Peak Memory % ©195 %Filter bv requestsvAvg. response262 ms 140 req/s25017•08.25|17-08-21Dorformonso dotolle fortotol durotinnPOST search contact by email Copy17•08•2717•08•4617•08•496,550108.480.0017:08:55Falture %0.0017•09•01Resp. time (Ava. ms)17:09:0717.09-1217.09-19- Requests/second - Ava. response - Error % - Virtual users ..• CPU % ••• Memory %Min (ms)Max (ms)693287Globals Vault Tools 5000...
|
NULL
|
-5589407268771755886
|
NULL
|
visual_change
|
ocr
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate wititheir internal logs if you ever need to escalate. Capture it:scorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', ["policv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter - these are present on 200 responses (just confirmedin your earlier Postman screnshot showing Max=110).The asvmmetry is the kev insight: HubSpot tells vou a lot when things are fine.almost nothing when they're not. Your limiter has to be smart enough to work withunan*x Hubspot• SearchYour team is now on the Free plan with 1 admin. You retain editing access and other members are read-only. View team permissions to see who can edit, or upgrade to restore collaboration.POST search de • POST Readabal • POST Reada bat eGET ReadGET ReadGET read call•GET Get Engage•teration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileSummaryTotal requests sent ©Requests/second ©Avg. response time ©6.550109.04155 ms% 100P90 ©173 msp95 O196 ms"Lukas sterka 121• In 1omGET Read CopyGET https://api.tIteration run HSPOST search contaPOST search conta© Iteration run Se100% L2Thu 7 May 17:12:40UparadeIteration run SeNo environmentWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotlteration run HSGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engagemente old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesystem may not be able to generate the loadneeded for this test and the cest is likely to§ Connect Git E Console E TerrP99 ©287 msError % ©0,00Failure % ©0,00Peak CPU % ©100.0 %Peak Memory % ©195 %Filter bv requestsvAvg. response262 ms 140 req/s25017•08.25|17-08-21Dorformonso dotolle fortotol durotinnPOST search contact by email Copy17•08•2717•08•4617•08•496,550108.480.0017:08:55Falture %0.0017•09•01Resp. time (Ava. ms)17:09:0717.09-1217.09-19- Requests/second - Ava. response - Error % - Virtual users ..• CPU % ••• Memory %Min (ms)Max (ms)693287Globals Vault Tools 5000...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4596
|
168
|
5
|
2026-05-07T14:12:44.366837+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163164366_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate Wititheir internal logs if you ever need to escalate. Capture it:f Lukas/Stefka 121 • in 18 m100% C/)Thu 7 May 17:12:44*x HubspotQ SearchYour team is now on the Free olan with 1 admin. You retain editina access and other members are read-onlv. View team nermissions to see who can edit or unarade to restore collaborationGET ReadGET ReadGET read callGET Get Engageteration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileTesfgm lteration run HmIteration run SD Iteration run Selscorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', ["policv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter - these are present on 200 responses (just confirmedin your earlier Postman screenshot showing Max=110 ).The asymmetry is the key insight: HubSpot tells you a lot when things are fine,almost nothing when they're not. Your limiter has to be smart enough to work withunanFailure Trend Tests vs VUThis run has no test assertionsAdd test scriots to vour requests to see test results hereWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotv teration run HsGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engageronte old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesustem may not be able to generate the loadneeded for this test and the cest is likely to§ Connect Git E Console E TerGlobals Vault Tools s 000...
|
NULL
|
8792617236680805267
|
NULL
|
click
|
ocr
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate Wititheir internal logs if you ever need to escalate. Capture it:f Lukas/Stefka 121 • in 18 m100% C/)Thu 7 May 17:12:44*x HubspotQ SearchYour team is now on the Free olan with 1 admin. You retain editina access and other members are read-onlv. View team nermissions to see who can edit or unarade to restore collaborationGET ReadGET ReadGET read callGET Get Engageteration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileTesfgm lteration run HmIteration run SD Iteration run Selscorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', ["policv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter - these are present on 200 responses (just confirmedin your earlier Postman screenshot showing Max=110 ).The asymmetry is the key insight: HubSpot tells you a lot when things are fine,almost nothing when they're not. Your limiter has to be smart enough to work withunanFailure Trend Tests vs VUThis run has no test assertionsAdd test scriots to vour requests to see test results hereWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotv teration run HsGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engageronte old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesustem may not be able to generate the loadneeded for this test and the cest is likely to§ Connect Git E Console E TerGlobals Vault Tools s 000...
|
4595
|
NULL
|
NULL
|
NULL
|
|
4597
|
167
|
1
|
2026-05-07T14:12:44.458576+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163164458_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(ahlLukas/Stefka 121 • in 18 m100% <478Thu 7 May 17:12:44DOCKERO ₴1DEV (docker)182Matching contact 4Matching contact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0Matching contact 1Matching contact 2Matching contact 3Matching contact 4Matchingcontact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0APP (-zsh)DEV (docker)*3T₴1-zsh• 84|screenpipe*•$5Jiminny \Exceptions \RateLimitExceptionHubspot returned 429at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206202'retry_after' =SretryAfter,203"reason'204= Se->getMessage(),205→ 206throw new RateLimitException('Hubspot returned 429', SretryAfter, $e);207208} else 1throw $e;209}210+14 vendor frames15-zsh₴6DEVapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166SevenShores\Hubspot\Http\Client:: request("POST", "[URL_WITH_CREDENTIALS]...
|
NULL
|
3083376211147240083
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(ahlLukas/Stefka 121 • in 18 m100% <478Thu 7 May 17:12:44DOCKERO ₴1DEV (docker)182Matching contact 4Matching contact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0Matching contact 1Matching contact 2Matching contact 3Matching contact 4Matchingcontact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0APP (-zsh)DEV (docker)*3T₴1-zsh• 84|screenpipe*•$5Jiminny \Exceptions \RateLimitExceptionHubspot returned 429at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206202'retry_after' =SretryAfter,203"reason'204= Se->getMessage(),205→ 206throw new RateLimitException('Hubspot returned 429', SretryAfter, $e);207208} else 1throw $e;209}210+14 vendor frames15-zsh₴6DEVapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166SevenShores\Hubspot\Http\Client:: request("POST", "[URL_WITH_CREDENTIALS]...
|
4592
|
NULL
|
NULL
|
NULL
|
|
4598
|
167
|
2
|
2026-05-07T14:12:47.183365+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163167183_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(ahlLukas/Stefka 121 • in 18 m100% <478Thu 7 May 17:12:47DOCKERO ₴1DEV (docker)182Matching contact 4Matching contact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0Matching contact 1Matching contact 2Matching contact 3Matching contact 4Matchingcontact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0APP (-zsh)DEV (docker)*3T₴1-zsh• 84|screenpipe*•$5Jiminny \Exceptions \RateLimitExceptionHubspot returned 429at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206202'retry_after' =SretryAfter,203"reason'204= Se->getMessage(),205→ 206throw new RateLimitException('Hubspot returned 429', SretryAfter, $e);207208} else 1throw $e;209}210+14 vendor frames15-zsh₴6DEVapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166SevenShores\Hubspot\Http\Client:: request("POST", "[URL_WITH_CREDENTIALS]...
|
NULL
|
-8809357114133920795
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp(ahlLukas/Stefka 121 • in 18 m100% <478Thu 7 May 17:12:47DOCKERO ₴1DEV (docker)182Matching contact 4Matching contact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0Matching contact 1Matching contact 2Matching contact 3Matching contact 4Matchingcontact 5Matching contact 6Matching contact 7Matching contact 8Matching contact 9root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugMatching contact 0APP (-zsh)DEV (docker)*3T₴1-zsh• 84|screenpipe*•$5Jiminny \Exceptions \RateLimitExceptionHubspot returned 429at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206202'retry_after' =SretryAfter,203"reason'204= Se->getMessage(),205→ 206throw new RateLimitException('Hubspot returned 429', SretryAfter, $e);207208} else 1throw $e;209}210+14 vendor frames15-zsh₴6DEVapp/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166SevenShores\Hubspot\Http\Client:: request("POST", "[URL_WITH_CREDENTIALS]...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4599
|
168
|
6
|
2026-05-07T14:12:47.224603+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163167224_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate Wititheir internal logs if you ever need to escalate. Capture it:f Lukas/Stefka 121 • in 18 m100% L2Thu 7 May 17:12:47*x HubspotQ SearchYour team is now on the Free olan with 1 admin. You retain editina access and other members are read-onlv. View team nermissions to see who can edit or unarade to restore collaborationGET ReadGET ReadGET read callGET Get Engagem lteration run HmIteration run SD Iteration run Selteration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profilescorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', ["policv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter - these are present on 200 responses (just confirmedin your earlier Postman screenshot showing Max=110 ).The asymmetry is the key insight: HubSpot tells you a lot when things are fine,almost nothing when they're not. Your limiter has to be smart enough to work withunanWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotv teration run HsGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engageronte old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesustem may not be able to generate the loadneeded for this test and the cest is likely to§ Connect Git E Console E TerThis run has no errorsAll requests were sent successfully and returned a 2xx response code.Globals Vault Tools S 0 0 0...
|
NULL
|
2296870113511451691
|
NULL
|
click
|
ocr
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate Wititheir internal logs if you ever need to escalate. Capture it:f Lukas/Stefka 121 • in 18 m100% L2Thu 7 May 17:12:47*x HubspotQ SearchYour team is now on the Free olan with 1 admin. You retain editina access and other members are read-onlv. View team nermissions to see who can edit or unarade to restore collaborationGET ReadGET ReadGET read callGET Get Engagem lteration run HmIteration run SD Iteration run Selteration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profilescorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', ["policv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter - these are present on 200 responses (just confirmedin your earlier Postman screenshot showing Max=110 ).The asymmetry is the key insight: HubSpot tells you a lot when things are fine,almost nothing when they're not. Your limiter has to be smart enough to work withunanWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotv teration run HsGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engageronte old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesustem may not be able to generate the loadneeded for this test and the cest is likely to§ Connect Git E Console E TerThis run has no errorsAll requests were sent successfully and returned a 2xx response code.Globals Vault Tools S 0 0 0...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4600
|
168
|
7
|
2026-05-07T14:12:52.701375+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778163172701_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate Wititheir internal logs if you ever need to escalate. Capture it:"Lukas sterka 121 • In 10m100% L2Thu 7 May 17:12:52*x Hubspot• SearchYour team is now on the Free olan with 1 admin. You retain editina access and other members are read-onlv. View team nermissions to see who can edit or unarade to restore collaborationGET ReadGET ReadGET read callGET Get Enoagem lteration run HmIteration run SE Iteration ruscorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', ["policv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter - these are present on 200 responses (just confirmedin your earlier Postman screenshot showing Max=110 ).The asymmetry is the key insight: HubSpot tells you a lot when things are fine,almost nothing when they're not. Your limiter has to be smart enough to work withunanWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotv teration run HsGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engageronte old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesustem may not be able to generate the loadneeded for this test and the cest is likely to§ Connect Git E Console E Terteration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileSummaryThis run has no errorsAll requests were sent successfully and returned a 2xx response code.Globals Vault Tools S 0 0 0...
|
NULL
|
474340234681381243
|
NULL
|
click
|
ocr
|
NULL
|
PostmancaltVIewWindowmelpHubSpot rate limit implem PostmancaltVIewWindowmelpHubSpot rate limit implementation strategy vfrom the response (and there's also one in HubSpot's response body for 429s) letsyou ask Hudspot support "why did this specine call get rate limited" or correlate Wititheir internal logs if you ever need to escalate. Capture it:"Lukas sterka 121 • In 10m100% L2Thu 7 May 17:12:52*x Hubspot• SearchYour team is now on the Free olan with 1 admin. You retain editina access and other members are read-onlv. View team nermissions to see who can edit or unarade to restore collaborationGET ReadGET ReadGET read callGET Get Enoagem lteration run HmIteration run SE Iteration ruscorrelationid = sthis->headerValue(Sheaders, 'x-hubspot-correlation-id')?? ($payload['correlationId'] ?? null);$this->log->warning('[Hubspot] 429 received', ["policv"= $policy,'retry_after'=> sdelayseconds,'hubspot_correlation_id''endpoint'→> $correlationId,=> $endpointForLogging, // pass this in]);When a customer reports "my sync was slow today" and you can correlate to a flurryof 429s with HubSpot correlation IDs, the diagnosis path is much shorter.So the final pictureFor this codebase, given what HubSpot actually sends:1. Pre-acquire from Redis bucket before every call — this is your primary defensesince post-hoc info is sparse.2. On 429, read policyName from the body — the only reliable signal of whichbucker hired.3. Apply policy-specific backoff with jitter - Retry-After is nice-to-have butrare in practice.4. On success, capture X-HubSpot-RateLimit-* headers via Guzzle middlewareto clamp the local counter - these are present on 200 responses (just confirmedin your earlier Postman screenshot showing Max=110 ).The asymmetry is the key insight: HubSpot tells you a lot when things are fine,almost nothing when they're not. Your limiter has to be smart enough to work withunanWrite a message.Opus 4. AdaptiveV COLLECTIONS> CRM Owners› CRM Pipelines› Dealsengagements>D OLD ENGAGEMENTSuer list meetingsPOST search modified companiesGET read call> POST search callsGET ist callsPOST meetings scheduledGET det meetingPost get link to task>POST Create Contact with Association› Hubspotv teration run HsGET Read Copyeg. An error occurred.successful operationv Iteration run Search HSpost search contact oy emall copy> Journal & webhoooks v4› OAuth› Properties> RESEARCHSEARCHIPoSt search contact by phonePOST search contact by emailPOST search meetings> PoST Search calls v3POST Search related meetinas v3Post search deals> Ticketsv UicofullGET engageronte old accociated hu dealSustem Resource WarningSustem resources are constrained. Thesustem may not be able to generate the loadneeded for this test and the cest is likely to§ Connect Git E Console E Terteration run Search HS (#7)u Iteration run Search HS • 20 VUs • May 07, 2026 17:08:21 (1 min) • Fixed profileSummaryThis run has no errorsAll requests were sent successfully and returned a 2xx response code.Globals Vault Tools S 0 0 0...
|
4599
|
NULL
|
NULL
|
NULL
|