$method, 'timeout' => 12, 'headers' => [ 'Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json', ], ]; if ( null !== $body ) { $args['body'] = wp_json_encode( $body ); } $response = wp_remote_request( self::BASE_URL . $path, $args ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code >= 400 ) { // HubSpot's error payload has a "message" field (e.g. "This // account does not have any more contacts allowed") — the same // "surface the real reason" treatment Mailchimp_Client gives // its own "detail" field. $message = is_array( $data ) && ! empty( $data['message'] ) ? $data['message'] : sprintf( /* translators: %d: HTTP status code */ __( 'HubSpot returned an unexpected error (HTTP %d).', 'chargeforms' ), $code ); return new \WP_Error( 'chargeforms_hubspot_api_error', $message, [ 'status' => $code, 'body' => $data ] ); } return is_array( $data ) ? $data : []; } /** Connection test — lists at most 1 contact, the cheapest real authenticated call available (no dedicated "whoami" endpoint on this API surface). */ public static function ping( $token ) { $result = self::request( $token, 'GET', '/crm/v3/objects/contacts?limit=1' ); return is_wp_error( $result ) ? $result : true; } /** * Create-or-update by a unique property (always 'email' here — every * HubSpot portal has this built-in, unlike a custom property that might * not exist yet in a given account). Idempotent by design, same * "resubmission never creates a duplicate" guarantee * Mailchimp_Client::upsert_member() gives via its own PUT-by-hash. */ public static function upsert_contact( $token, $email, array $properties ) { $properties['email'] = $email; $result = self::request( $token, 'POST', '/crm/v3/objects/contacts/batch/upsert', [ 'inputs' => [ [ 'id' => $email, 'idProperty' => 'email', 'properties' => $properties ], ], ] ); if ( is_wp_error( $result ) ) { return $result; } // A partial failure inside an otherwise-200 batch response still // needs surfacing as an error — HubSpot's batch endpoints return // per-item errors rather than an overall non-2xx status for this. if ( ! empty( $result['errors'] ) ) { $first = $result['errors'][0]; return new \WP_Error( 'chargeforms_hubspot_api_error', $first['message'] ?? __( 'HubSpot rejected this contact.', 'chargeforms' ), [ 'status' => 400, 'body' => $result ] ); } return $result; } }